# Transfer Job GQL Test Queries

## QA test data (seeded by python-rds-utils PORT-101)

| product | release_id | project_id | origin vendor | destination vendor |
|---|---|---|---|---|
| Life Of The Party | 956820 | 3135437 | 21989 (sub 7645) | 21786 → 15063 |
| Drinks After Work | 2380732 | 3977109 | 21989 (sub 28676) | 21786 |
| Live-Móvel (Ao Vivo) | 2393183 | 3983514 | 21786 | 21989 (sub 109765) |

## Known field restrictions in local dev

Do NOT include these fields - they cause null propagation or 400s:

| Field | Why |
|---|---|
| `following` on any Label | Calls ows-notifications → 403 → nulls non-null field → nulls parent |
| `assignedToEmail` on Vendor | Calls ows-account directly → 400 blocked through ows-grass |
| `product { artists { ... } }` | `artists: [Artist]!` non-null → ows-product-digital 403 → nulls entire `product` |

Safe product fields: `productId productName vendorId subaccountId label { vendorId subaccountId }`

---

## Fundamental problems

### 1. `authorization.py` identity check bug (fixed locally)

`if identity_err:` in the `authorize_transfer_job` decorator should be `if identity_err is not None:`. oto Response objects are falsy even for error statuses, so a 401 response was silently skipped, passing `identity=None` into handlers. This caused the `created_by_identity_id cannot be null` 500 in local dev. Fixed in `ows-project-manager/project_manager/util/authorization.py`.

### 2. What cannot be tested via GQL

The SFN-driven operations are service-to-service only - not exposed as GQL mutations:

- `PATCH /transfer/job/<id>` - update status / failure_reason (SFN step)
- `PATCH /transfer/job/<id>/products` - set destination_artist_id per release (SFN step)
- `DELETE /transfer/job/<id>` - soft-delete a QUEUED job (admin only)
- `POST /transfer/batch/execute` - returns 501 (PORT-69 follow-up)

So GQL can only test the QUEUED state. PROCESSING/COMPLETED/FAILED states require direct REST calls or a real SFN run.

---

## Suggested run order

1. Query 1 - see existing state
2. Mutation 9 - create a job, capture the returned `projectTransferJobId`
3. Query 7 - verify single job + products end to end with that ID
4. Queries 2/3/4/5/6 - filter/pagination coverage
5. Query 8 - null check for nonexistent job
6. Mutations 10/11 - verify typed error union paths

---

## 1. List all jobs (no filter)

```graphql
query ListAllTransferJobs {
  projectTransferJobs {
    totalCount
    items {
      projectTransferJobId
      status
      createdAt
      lastUpdatedAt
      failureReason
      transferCompletedOn
      revenueCutoffDate
      createdBy {
        id
      }
      originLabel {
        vendorId
        subaccountId
      }
      destinationLabel {
        vendorId
        subaccountId
      }
      project {
        projectId
      }
    }
  }
}
```

---

## 2. List jobs filtered by origin vendor

```graphql
query ListJobsByOriginVendor {
  projectTransferJobs(originVendorId: "21989") {
    totalCount
    items {
      projectTransferJobId
      status
      originLabel { vendorId subaccountId }
      destinationLabel { vendorId subaccountId }
    }
  }
}
```

---

## 3. List jobs filtered by destination vendor

```graphql
query ListJobsByDestinationVendor {
  projectTransferJobs(destinationVendorId: "21786") {
    totalCount
    items {
      projectTransferJobId
      status
      originLabel { vendorId subaccountId }
      destinationLabel { vendorId subaccountId }
    }
  }
}
```

---

## 4. List jobs filtered by status

```graphql
query ListQueuedJobs {
  projectTransferJobs(status: QUEUED, limit: 10) {
    totalCount
    items {
      projectTransferJobId
      status
      createdAt
      project { projectId }
    }
  }
}
```

---

## 5. List jobs filtered by project

```graphql
query ListJobsByProject {
  projectTransferJobs(projectId: "3977109", originVendorId: "21989") {
    totalCount
    items {
      projectTransferJobId
      status
      originLabel { vendorId subaccountId }
      destinationLabel { vendorId subaccountId }
    }
  }
}
```

Combine with `originVendorId` to pin the exact transfer (a project can be transferred multiple times, so `projectId` alone may return history).

---

## 6. Pagination (second page)

Verifies limit/offset wiring. Use `limit: 1` to force pagination with small data sets.

```graphql
query PaginatedJobs {
  projectTransferJobs(limit: 1, offset: 0) {
    totalCount
    items {
      projectTransferJobId
      status
    }
  }
}
```

Then bump `offset: 1` and confirm a different job comes back and `totalCount` stays the same.

---

## 7. Get single job with full product list

Replace `"1"` with a real job ID from query 1 or from the mutation response.

```graphql
query GetTransferJobWithProducts {
  projectTransferJob(jobId: "1") {
    projectTransferJobId
    status
    failureReason
    transferCompletedOn
    revenueCutoffDate
    createdAt
    lastUpdatedAt
    createdBy {
      id
    }
    originLabel {
      vendorId
      subaccountId
    }
    destinationLabel {
      vendorId
      subaccountId
    }
    project {
      projectId
    }
    products {
      productTransferHistoryId
      projectTransferJobId
      status
      failureReason
      transferCompletedOn
      product {
        productId
        productName
        vendorId
        subaccountId
        label {
          vendorId
          subaccountId
        }
      }
    }
  }
}
```

---

## 8. Get nonexistent job (null check)

Should return `{ data: { projectTransferJob: null } }` with no errors - not a GQL error.

```graphql
query GetNonexistentJob {
  projectTransferJob(jobId: "999999999") {
    projectTransferJobId
    status
  }
}
```

---

## 9. Create a transfer job (success path)

Transfers project 3977109 from vendor 21989/subaccount 28676 to vendor 21786 - matches QA scenario for product 2.

```graphql
mutation CreateTransferJob {
  createProjectTransferJob(input: {
    projectId: "3977109"
    originVendorId: "21989"
    originSubaccountId: "28676"
    destinationVendorId: "21786"
    revenueCutoffDate: "2025-12-31"
  }) {
    ... on ProjectTransferJob {
      projectTransferJobId
      status
      createdAt
      originLabel { vendorId subaccountId }
      destinationLabel { vendorId subaccountId }
      project { projectId }
      products {
        productTransferHistoryId
        product {
          productId
          productName
        }
      }
    }
    ... on CreateProjectTransferJobError {
      code
      message
    }
  }
}
```

**Expected:** `ProjectTransferJob` branch with `status: QUEUED` and at least one product entry.

---

## 10. Create a transfer job - empty project (bad_request error)

Use a project ID that exists but has no releases, or a nonexistent project.

```graphql
mutation CreateTransferJobNoReleases {
  createProjectTransferJob(input: {
    projectId: "1"
    originVendorId: "21989"
    destinationVendorId: "21786"
  }) {
    ... on ProjectTransferJob {
      projectTransferJobId
      status
    }
    ... on CreateProjectTransferJobError {
      code
      message
    }
  }
}
```

**Expected:** `CreateProjectTransferJobError` branch with `code: "bad_request"`.

---

## 11. Create a transfer job - invalid vendor (FK violation typed error)

Nonexistent vendor IDs hit a FK constraint on `project_transfer_job`. ows-project-manager catches MySQL error 1216/1452 in the SQLAlchemy error handler and returns 400; the connector converts it to a typed error.

```graphql
mutation CreateTransferJobInvalidInput {
  createProjectTransferJob(input: {
    projectId: "3977109"
    originVendorId: "99999999"
    destinationVendorId: "99999998"
  }) {
    ... on ProjectTransferJob {
      projectTransferJobId
      status
    }
    ... on CreateProjectTransferJobError {
      code
      message
    }
  }
}
```

**Expected:** `CreateProjectTransferJobError` with `code: "bad_request"` and `message: "Invalid vendor or reference ID."`.

**Note:** in production PDP rejects unknown vendor IDs as 403 before the INSERT runs, so this error path is local-dev only.
