Skip to content

Feature 323 - Third party subscription fulfilment - current - #477

Merged
junaidiqbalmoj merged 40 commits into
masterfrom
feature/323-third-party-subscription-fulfilment
Jun 5, 2026
Merged

Feature 323 - Third party subscription fulfilment - current#477
junaidiqbalmoj merged 40 commits into
masterfrom
feature/323-third-party-subscription-fulfilment

Conversation

@alao-daniel

@alao-daniel alao-daniel commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

[VIBE-367] Third Party subscription Fulfilment - Current #323

Change description

Implement third party subscription fulfilment
Closes #323

Checklist

  • commit messages are meaningful and follow good commit message guidelines
  • README and other documentation has been updated / added (if needed)
  • tests have been updated / new tests has been added (if needed)
  • Does this PR introduce a breaking change

Summary by CodeRabbit

  • New Features

    • Added third‑party subscription fulfilment: sends HTTP pushes (including deletion notifications) to external endpoints when hearing lists are published, updated or removed; supports sending generated PDFs or flat‑file attachments.
    • Uses certificate‑based authentication sourced from Key Vault; distinguishes new vs updated publications and logs all push attempts/responses.
  • Documentation

    • Added comprehensive technical plan, review and task docs for the third‑party fulfilment feature.
  • Chores

    • Deployment values extended to surface required Courtel secrets.

github-actions Bot and others added 9 commits February 11, 2026 12:33
Created technical specification and task list for implementing third-party
subscription fulfilment with HTTPS push notifications. Plan includes:
- New libs/third-party-fulfilment module
- Database schema for subscriptions and push audit logs
- Certificate-based authentication with Azure Key Vault
- Retry logic with exponential backoff
- Custom headers for P&I push API
- Integration with existing upload flows

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new third‑party fulfilment subsystem is added via libs/legacy-third-party-fulfilment, implementing HTTPS push delivery with certificate auth, retry/backoff, subscriber lookup, push logging (Prisma + migration), and integrations into publication processing and list‑removal flows; Helm values and TS path aliases updated accordingly.

Changes

Cohort / File(s) Summary
Third‑Party Fulfilment module
libs/legacy-third-party-fulfilment/package.json, libs/legacy-third-party-fulfilment/tsconfig.json, libs/legacy-third-party-fulfilment/src/config.ts, libs/legacy-third-party-fulfilment/src/index.ts
New workspace package with build/test scripts, exported entrypoints and prisma schema path exports.
Prisma model & DB migration
libs/legacy-third-party-fulfilment/prisma/schema.prisma, apps/postgres/prisma/migrations/..._add_legacy_third_party_push_log/migration.sql
Adds ThirdPartyPushLog / legacy_third_party_push_log table and indexes on artefact_id and list_type_id.
Push infra (headers, HTTP, retry)
libs/legacy-third-party-fulfilment/src/push/headers.ts, .../headers.test.ts, libs/legacy-third-party-fulfilment/src/push/http-client.ts, .../http-client.test.ts, libs/legacy-third-party-fulfilment/src/push/retry.ts, .../retry.test.ts
Builds push headers, TLS HTTPS client with JSON/multipart handling, and retry logic (3 attempts, skip 4xx except 429) plus tests.
Subscriber queries & service
libs/legacy-third-party-fulfilment/src/queries.ts, .../queries.test.ts, libs/legacy-third-party-fulfilment/src/service.ts, .../service.test.ts
Find subscribers by listType/sensitivity, orchestrate pushes (publications & deletions), resolve secrets, fetch location, and log push outcomes.
Publication processing integration
libs/publication/src/processing/service.ts, .../service.test.ts, libs/publication/src/repository/model.ts, libs/publication/src/repository/queries.ts, .../queries.test.ts, libs/publication/package.json
processPublication signature extended (sensitivity, language, isUpdate, flatFilePath, skipThirdPartyPush); async call to sendThirdPartyPublications; Artefact gains supersededCount; createArtefact now returns { artefactId, isUpdate }.
Admin upload & removal handlers
libs/admin-pages/src/pages/manual-upload-summary/index.ts, .../index.test.ts, libs/admin-pages/src/pages/non-strategic-upload-summary/index.ts, .../index.test.ts, libs/admin-pages/src/pages/remove-list-confirmation/index.ts, libs/admin-pages/package.json
Upload flows destructure and forward isUpdate; removal flow retrieves artefacts and invokes sendThirdPartyDeletion per artefact (errors logged).
Blob ingestion updates
libs/api/src/blob-ingestion/repository/service.ts, .../service.test.ts, libs/api/src/blob-ingestion/file-storage.ts
Ingestion handler propagates { artefactId, isUpdate } and now returns saved file path from file-storage function.
Helm values & Key Vault mappings
apps/web/helm/values.yaml, apps/web/helm/values.dev.yaml, apps/api/helm/values.dev.yaml
Adds Courtel secret mappings (COURTEL_API_URL, COURTEL_CERTIFICATE) and minor secret alias newline change.
TS config & schema discovery
tsconfig.json, apps/postgres/src/schema-discovery.ts, apps/postgres/src/schema-discovery.test.ts
Adds @hmcts/legacy-third-party-fulfilment path alias and includes its prisma schemas in discovery/tests.
New tests & utilities
many ...test.ts under libs/legacy-third-party-fulfilment and updates in existing tests
Extensive unit tests for headers, HTTP client, retry, queries, service behaviour; existing tests updated to new artefact return shape and flows.
Docs
docs/tickets/323/plan.md, docs/tickets/323/review.md, docs/tickets/323/tasks.md, docs/tickets/323/ticket.md
Design, plan, review/acceptance and task tracking for Third‑Party subscription fulfilment.
Minor cleanup
libs/list-types/.../index.test.ts, libs/location/src/seed-list-types.ts
Test import cleanup and a local variable rename.

Sequence Diagram(s)

sequenceDiagram
    participant User as Publisher/System
    participant PubSvc as Publication Service
    participant ThirdSvc as ThirdParty Fulfilment
    participant SubQuery as Subscriber Query
    participant LocSvc as Location Service
    participant Header as Header Builder
    participant HTTP as HTTP Client (retry)
    participant DB as Push Log (Prisma)
    participant External as Third‑party API

    User->>PubSvc: processPublication(params...)
    PubSvc->>PubSvc: generate PDF / prepare payload
    PubSvc->>ThirdSvc: sendThirdPartyPublications(params) (async)
    ThirdSvc->>ThirdSvc: resolve COURTEL_API_URL & certificate
    ThirdSvc->>SubQuery: findSubscribersByListType(listTypeId, sensitivity)
    SubQuery-->>ThirdSvc: [subscribers]
    ThirdSvc->>LocSvc: getLocationWithDetails(locationId)
    LocSvc-->>ThirdSvc: location details
    ThirdSvc->>Header: buildPushHeaders(metadata)
    Header-->>ThirdSvc: headers
    ThirdSvc->>HTTP: pushWithRetry(url, cert, headers, body, pdfPath/flatFile)
    HTTP->>External: HTTPS POST (with retries/backoff)
    External-->>HTTP: statusCode
    HTTP-->>ThirdSvc: {statusCode, success}
    ThirdSvc->>DB: prisma.thirdPartyPushLog.create({...})
    DB-->>ThirdSvc: ack
Loading
sequenceDiagram
    participant User as Admin UI
    participant Remove as RemoveListHandler
    participant Query as Artefact Query
    participant Delete as Delete DB
    participant ThirdSvc as ThirdParty Fulfilment
    participant HTTP as HTTP Client (retry)
    participant External as Third‑party API
    participant DB as Push Log

    User->>Remove: confirm deletion([ids])
    Remove->>Query: getArtefactsByIds(ids)
    Query-->>Remove: artefact metadata
    Remove->>Delete: deleteArtefacts(ids)
    Delete-->>Remove: success
    loop each artefact
        Remove->>ThirdSvc: sendThirdPartyDeletion(artefact metadata) (async)
        ThirdSvc->>ThirdSvc: resolve secrets & find subscribers
        ThirdSvc->>HTTP: pushWithRetry(url, cert, headers, null)
        HTTP->>External: HTTPS POST (null body)
        External-->>HTTP: statusCode
        HTTP-->>ThirdSvc: {statusCode, success}
        ThirdSvc->>DB: create push log (DELETION)
    end
    Remove-->>User: deletion confirmed
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature 323 - Third party subscription fulfilment - current' directly references the main feature implementation described across the changeset.
Linked Issues check ✅ Passed The PR implements all primary objectives from issue #323: third-party push delivery with certificate auth [#323], header inclusion and location data [#323], retry logic [#323], deletion handling [#323], subscriber matching [#323], and comprehensive tests [#323].
Out of Scope Changes check ✅ Passed All changes are scoped to issue #323 objectives: new legacy-third-party-fulfilment module, database schema/migrations, integration into publication flow, Helm configuration updates, and supporting test/documentation files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/323-third-party-subscription-fulfilment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread .claude/hooks/post-write.sh
Comment thread .github/workflows/claude.yml
Comment thread apps/api/package.json Outdated
Comment thread apps/api/package.json
@alao-daniel
alao-daniel changed the base branch from feature/322-third-party-user-management to master April 15, 2026 11:33
…arty-subscription-fulfilment

# Conflicts:
#	apps/web/helm/values.dev.yaml
#	libs/admin-pages/src/pages/manual-upload-summary/index.test.ts
#	libs/admin-pages/src/pages/remove-list-confirmation/index.ts
@github-actions

github-actions Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright E2E Test Results

84 tests   51 ✅  5m 39s ⏱️
33 suites  33 💤
 1 files     0 ❌

Results for commit 81c1c8c.

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/admin-pages/src/pages/manual-upload-summary/index.ts (1)

130-141: ⚠️ Potential issue | 🟠 Major

Variable shadowing causes jsonData to always be undefined in processPublication.

Line 130 declares let jsonData: unknown; but line 133 redeclares const jsonData inside the try block, shadowing the outer variable. The outer jsonData (passed to processPublication at line 150) will never be assigned.

Proposed fix
     let jsonData: unknown;
     if (!isFlatFile) {
       try {
-        const jsonData = JSON.parse(uploadData.file.toString("utf-8"));
+        jsonData = JSON.parse(uploadData.file.toString("utf-8"));
         await extractAndStoreArtefactSearch(artefactId, listTypeId, jsonData);
       } catch (error) {
🧹 Nitpick comments (10)
libs/api/src/blob-ingestion/repository/service.test.ts (1)

64-64: Add one update-path test for isUpdate: true.

The revised tests only exercise isUpdate: false. Please add a companion case asserting processPublication receives isUpdate: true when createArtefact reports an update.

As per coding guidelines: "Aim for >80% test coverage on business logic."

Also applies to: 91-91, 159-159, 178-178, 199-199, 299-315, 327-327, 347-347, 376-376

libs/admin-pages/src/pages/non-strategic-upload-summary/index.test.ts (1)

76-77: Please cover the isUpdate: true branch in POST tests.

Current mocks pin isUpdate to false; add a case that verifies the handler forwards isUpdate: true into processPublication.

As per coding guidelines: "Aim for >80% test coverage on business logic."

Also applies to: 405-405

docs/tickets/323/review.md (1)

52-52: Minor documentation inconsistency.

The line reference index.ts:118-137 appears outdated. In the current code, the deletion integration spans lines 134-148. Consider updating for accuracy.

📝 Suggested fix
-- **Deletion integration** correctly wired into `libs/admin-pages/src/pages/remove-list-confirmation/index.ts:118-137` — fetches artefact metadata before deletion, then calls `sendThirdPartyDeletion` fire-and-forget for each deleted artefact.
+- **Deletion integration** correctly wired into `libs/admin-pages/src/pages/remove-list-confirmation/index.ts:134-148` — fetches artefact metadata before deletion, then calls `sendThirdPartyDeletion` fire-and-forget for each deleted artefact.
libs/third-party-fulfilment/src/push/retry.ts (1)

5-5: Consider using Set for status code lookup.

Per static analysis, SUCCESS_STATUSES.has() is more idiomatic than array .includes() for membership checks.

♻️ Suggested refactor
-const SUCCESS_STATUSES = [200, 201, 202, 204];
+const SUCCESS_STATUSES = new Set([200, 201, 202, 204]);

And at line 43:

-    if (SUCCESS_STATUSES.includes(lastResult.statusCode)) {
+    if (SUCCESS_STATUSES.has(lastResult.statusCode)) {

Also applies to: 43-43

libs/third-party-fulfilment/src/queries.ts (1)

12-16: Consider removing unused user relation include.

Based on libs/third-party-fulfilment/src/service.ts (lines 67 and 89), the subscriber query result is only checked for .length — individual subscriber or user data is never accessed. The push goes to a single centralised API endpoint.

Removing include: { user: true } would reduce query overhead.

♻️ Suggested change
 export async function findSubscribersByListType(listTypeId: number, sensitivity: string) {
   return prisma.legacyThirdPartySubscription.findMany({
-    where: { listTypeId, sensitivity: { in: eligibleSensitivities(sensitivity) } },
-    include: { user: true }
+    where: { listTypeId, sensitivity: { in: eligibleSensitivities(sensitivity) } }
   });
 }
docs/tickets/323/plan.md (1)

63-81: Minor: Add language specifier to fenced code block.

The code block starting at line 63 lacks a language identifier, which affects syntax highlighting.

📝 Suggested fix
-```
+```text
 libs/third-party-fulfilment/
 ├── package.json
libs/third-party-fulfilment/src/push/http-client.ts (1)

29-29: Consider destroying the HTTPS agent after use.

The https.Agent is created per request but never explicitly destroyed. While Node.js will clean up eventually, explicitly calling agent.destroy() after the request completes would release resources sooner, especially under high load.

♻️ Proposed fix
   return new Promise((resolve, reject) => {
     const req = https.request(options, (res) => {
       // Drain the response body to free the socket
       res.resume();

       res.on("end", () => {
         const statusCode = res.statusCode ?? 0;
+        agent.destroy();
         resolve({ statusCode, success: isSuccessStatus(statusCode) });
       });
     });

     req.on("timeout", () => {
       req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
     });

-    req.on("error", reject);
+    req.on("error", (err) => {
+      agent.destroy();
+      reject(err);
+    });
libs/third-party-fulfilment/src/service.test.ts (1)

46-53: Consider cleaning up environment variables after tests.

The beforeEach sets process.env.COURTEL_API_URL and process.env.COURTEL_CERTIFICATE, but these aren't cleaned up in afterEach. This could leak state to other test files.

♻️ Proposed fix
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { beforeEach, describe, expect, it, vi } from "vitest";

+const originalEnv = { ...process.env };
+
 beforeEach(() => {
   vi.resetAllMocks();
   vi.mocked(pushWithRetry).mockResolvedValue({ statusCode: 200, success: true });
   vi.mocked(findSubscribersByListType).mockResolvedValue([{ id: "sub-1", user: { id: "user-1", name: "courtel" } }] as never);
   mockPushLogCreate.mockResolvedValue({});
   process.env.COURTEL_API_URL = "https://courtel.example.com/api";
   process.env.COURTEL_CERTIFICATE = Buffer.from("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----").toString("base64");
 });
+
+afterEach(() => {
+  process.env = { ...originalEnv };
+});
docs/tickets/323/tasks.md (2)

1-1: Use consistent compound hyphenation in the title.

Line 1 would read more cleanly as “Third-Party Subscription Fulfilment” for consistent compound-adjective style.


55-56: Add verifiable test evidence alongside pass/coverage claims.

Line 55 and Line 56 state outcomes, but there is no pointer to artefacts (for example, command output path, coverage report location, or CI job link). Adding one makes the checklist auditable.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0d5b62bf-c095-4f0b-b987-f802112fb41b

📥 Commits

Reviewing files that changed from the base of the PR and between 5045f89 and 35f9893.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (41)
  • apps/api/helm/values.dev.yaml
  • apps/api/helm/values.yaml
  • apps/postgres/prisma/migrations/20260409150532_add_legacy_third_party_push_log/migration.sql
  • apps/postgres/src/schema-discovery.test.ts
  • apps/postgres/src/schema-discovery.ts
  • apps/web/helm/values.dev.yaml
  • apps/web/helm/values.yaml
  • docs/tickets/323/plan.md
  • docs/tickets/323/review.md
  • docs/tickets/323/tasks.md
  • docs/tickets/323/ticket.md
  • libs/admin-pages/package.json
  • libs/admin-pages/src/pages/manual-upload-summary/index.test.ts
  • libs/admin-pages/src/pages/manual-upload-summary/index.ts
  • libs/admin-pages/src/pages/non-strategic-upload-summary/index.test.ts
  • libs/admin-pages/src/pages/non-strategic-upload-summary/index.ts
  • libs/admin-pages/src/pages/remove-list-confirmation/index.ts
  • libs/api/src/blob-ingestion/repository/service.test.ts
  • libs/api/src/blob-ingestion/repository/service.ts
  • libs/publication/package.json
  • libs/publication/src/processing/service.test.ts
  • libs/publication/src/processing/service.ts
  • libs/publication/src/repository/model.ts
  • libs/publication/src/repository/queries.test.ts
  • libs/publication/src/repository/queries.ts
  • libs/third-party-fulfilment/package.json
  • libs/third-party-fulfilment/prisma/schema.prisma
  • libs/third-party-fulfilment/src/config.ts
  • libs/third-party-fulfilment/src/index.ts
  • libs/third-party-fulfilment/src/push/headers.test.ts
  • libs/third-party-fulfilment/src/push/headers.ts
  • libs/third-party-fulfilment/src/push/http-client.test.ts
  • libs/third-party-fulfilment/src/push/http-client.ts
  • libs/third-party-fulfilment/src/push/retry.test.ts
  • libs/third-party-fulfilment/src/push/retry.ts
  • libs/third-party-fulfilment/src/queries.test.ts
  • libs/third-party-fulfilment/src/queries.ts
  • libs/third-party-fulfilment/src/service.test.ts
  • libs/third-party-fulfilment/src/service.ts
  • libs/third-party-fulfilment/tsconfig.json
  • tsconfig.json

**Pre-Condition**
* CaTH Third Party users can subscribe to receive specific hearing lists published in CaTH

**TECHNIAL SPECIFICATION**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: "TECHNIAL" should be "TECHNICAL".

📝 Suggested fix
- **TECHNIAL SPECIFICATION**
+ **TECHNICAL SPECIFICATION**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**TECHNIAL SPECIFICATION**
**TECHNICAL SPECIFICATION**

Comment on lines +46 to +51
* Successful <`**POST**`>(https://hmcts.github.io/restful~~api~~standards/#post) requests will generate:
** 200 (if resources have been updated)
** 201 (if resources have been created)
** 202 (if the request was accepted but has not been finished yet)
*** 204 with <`**Location*`>(https://tools.ietf.org/html/rfc7231#section-7.1.2) header (if the actual resource is not returned)
* Validation is established to ensure no publication is sent when the trigger has not been activated, to ensure that Non-subscribed JSON payload is not sent to the third party and to ensure that the system differentiates between newly uploaded and updated publication

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Markdown formatting issues in acceptance criteria.

The bullet list formatting appears malformed with mixed link syntax. Consider reviewing the markdown structure for proper rendering.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 47-47: Spaces inside emphasis markers

(MD037, no-space-in-emphasis)

"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assets to exclude to match the library tsconfig baseline.

assets is currently not excluded, so static assets can be pulled into compilation if present.

Suggested patch
-  "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules"]
+  "exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "assets"]

Based on learnings: "Applies to libs/*/tsconfig.json : Library modules must have tsconfig.json extending root tsconfig with outDir, rootDir, declaration, and declarationMap compiler options. Exclude test files, dist, node_modules, and assets from compilation."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules"]
"exclude": ["**/*.test.ts", "**/*.spec.ts", "dist", "node_modules", "assets"]

Comment thread apps/web/helm/values.yaml
- name: sso-sg-admin-local
alias: SSO_INTERNAL_ADMIN_LOCAL_GROUP_ID
# Third-party push secrets
- name: auto-pip-stg-courtel-api

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

COURTEL_API_URL and COURTEL_CERTIFICATE are in both web and api helm. Do they need to be in both helm charts?

@@ -0,0 +1,22 @@
generator client {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder we should name the lib legacy_third_party_fulfilment. The tables all prefixed with the word legacy_. Also when we add the new third-party stuff later, we can avoid confusion.

return {
"x-provenance": provenance,
"x-source-artefact-id": artefactId,
"x-type": listTypeName,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x-type can be LIST, GENERAL_PUBLICATIONS, etc. This normally comes from the Artefact table. I can see this field is currently missing in the AI Artefact table. For now, may be we can just hard code the value to be "LIST" because that is the only thing Courtel received

let bodyBuffer: Buffer;
const requestHeaders: Record<string, string | number> = { ...headers };

if (pdfPath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current behaviour is:

  • If it is a JSON publication, we will check if the PDF is generated or not. If yes, we will send them both the JSON publication, and the generated PDF. if no, we will just send them the JSON publication.
  • If it is a flat file publication, we will just send them the flat file just the content type of multipart form data.

Not sure if that is what it is doing currently as I can't really see those logic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
libs/legacy-third-party-fulfilment/src/push/retry.ts (1)

36-41: Swallowed error loses diagnostics.

The bare catch {} discards the underlying network error (e.g. ECONNREFUSED, TLS failure), making field diagnosis reliant on statusCode 0 alone. A one-liner log preserves the cause without changing retry behaviour.

♻️ Suggested tweak
-    try {
-      lastResult = await executePush(url, certPem, headers, body, pdfPath);
-    } catch {
-      // Network-level error — treat as statusCode 0 and retry
-      lastResult = { statusCode: 0, success: false };
-    }
+    try {
+      lastResult = await executePush(url, certPem, headers, body, pdfPath);
+    } catch (error) {
+      console.warn(`${logPrefix} Push attempt ${attempt + 1}/${MAX_ATTEMPTS} network error:`, error);
+      lastResult = { statusCode: 0, success: false };
+    }
libs/legacy-third-party-fulfilment/src/service.ts (1)

32-40: console.error for missing env vars overstates the severity.

A missing COURTEL_API_URL/COURTEL_CERTIFICATE is a "feature disabled / skipped" condition, not a failure — logging at error level will trigger ops alerting in environments where the integration isn't configured. console.warn (or info) reads more accurately given the message already says "skipped".


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0818c3af-4589-4d45-9922-0abe47c14cc8

📥 Commits

Reviewing files that changed from the base of the PR and between 35f9893 and 85aa112.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (26)
  • apps/postgres/src/schema-discovery.test.ts
  • apps/postgres/src/schema-discovery.ts
  • libs/admin-pages/package.json
  • libs/admin-pages/src/pages/remove-list-confirmation/index.ts
  • libs/legacy-third-party-fulfilment/package.json
  • libs/legacy-third-party-fulfilment/prisma/schema.prisma
  • libs/legacy-third-party-fulfilment/src/config.ts
  • libs/legacy-third-party-fulfilment/src/index.ts
  • libs/legacy-third-party-fulfilment/src/push/headers.test.ts
  • libs/legacy-third-party-fulfilment/src/push/headers.ts
  • libs/legacy-third-party-fulfilment/src/push/http-client.test.ts
  • libs/legacy-third-party-fulfilment/src/push/http-client.ts
  • libs/legacy-third-party-fulfilment/src/push/retry.test.ts
  • libs/legacy-third-party-fulfilment/src/push/retry.ts
  • libs/legacy-third-party-fulfilment/src/queries.test.ts
  • libs/legacy-third-party-fulfilment/src/queries.ts
  • libs/legacy-third-party-fulfilment/src/service.test.ts
  • libs/legacy-third-party-fulfilment/src/service.ts
  • libs/legacy-third-party-fulfilment/tsconfig.json
  • libs/list-types/civil-and-family-daily-cause-list/src/pages/index.test.ts
  • libs/location/src/seed-list-types.ts
  • libs/publication/package.json
  • libs/publication/src/processing/service.test.ts
  • libs/publication/src/processing/service.ts
  • libs/publication/src/repository/queries.ts
  • tsconfig.json
✅ Files skipped from review due to trivial changes (8)
  • libs/location/src/seed-list-types.ts
  • libs/publication/package.json
  • libs/legacy-third-party-fulfilment/tsconfig.json
  • libs/legacy-third-party-fulfilment/src/config.ts
  • libs/admin-pages/package.json
  • libs/legacy-third-party-fulfilment/src/index.ts
  • tsconfig.json
  • libs/legacy-third-party-fulfilment/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/postgres/src/schema-discovery.ts
  • apps/postgres/src/schema-discovery.test.ts
  • libs/publication/src/processing/service.test.ts
  • libs/publication/src/processing/service.ts
  • libs/publication/src/repository/queries.ts

Comment on lines +134 to +148
for (const artefact of artefactsToDelete) {
sendThirdPartyDeletion({
artefactId: artefact.artefactId,
locationId: artefact.locationId,
listTypeId: artefact.listTypeId,
contentDate: artefact.contentDate,
sensitivity: artefact.sensitivity,
language: artefact.language,
displayFrom: artefact.displayFrom,
displayTo: artefact.displayTo,
provenance: artefact.provenance
}).catch((error) => {
console.error("Third-party deletion push failed:", error);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fire-and-forget pushes risk being dropped after the response.

sendThirdPartyDeletion(...) is invoked without await, so the handler redirects (and the request/process may be torn down) before the HTTPS push, its retries, and the push-log write complete. In a containerised/serverless deployment this can silently lose deletion notifications and log rows. Consider awaiting the pushes (e.g. await Promise.allSettled(...)) before the redirect, or dispatching them to a durable queue/worker.

♻️ Suggested shape
-    for (const artefact of artefactsToDelete) {
-      sendThirdPartyDeletion({
-        artefactId: artefact.artefactId,
-        ...
-      }).catch((error) => {
-        console.error("Third-party deletion push failed:", error);
-      });
-    }
+    await Promise.allSettled(
+      artefactsToDelete.map((artefact) =>
+        sendThirdPartyDeletion({
+          artefactId: artefact.artefactId,
+          locationId: artefact.locationId,
+          listTypeId: artefact.listTypeId,
+          contentDate: artefact.contentDate,
+          sensitivity: artefact.sensitivity,
+          language: artefact.language,
+          displayFrom: artefact.displayFrom,
+          displayTo: artefact.displayTo,
+          provenance: artefact.provenance
+        }).catch((error) => {
+          console.error("Third-party deletion push failed:", error);
+        })
+      )
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const artefact of artefactsToDelete) {
sendThirdPartyDeletion({
artefactId: artefact.artefactId,
locationId: artefact.locationId,
listTypeId: artefact.listTypeId,
contentDate: artefact.contentDate,
sensitivity: artefact.sensitivity,
language: artefact.language,
displayFrom: artefact.displayFrom,
displayTo: artefact.displayTo,
provenance: artefact.provenance
}).catch((error) => {
console.error("Third-party deletion push failed:", error);
});
}
await Promise.allSettled(
artefactsToDelete.map((artefact) =>
sendThirdPartyDeletion({
artefactId: artefact.artefactId,
locationId: artefact.locationId,
listTypeId: artefact.listTypeId,
contentDate: artefact.contentDate,
sensitivity: artefact.sensitivity,
language: artefact.language,
displayFrom: artefact.displayFrom,
displayTo: artefact.displayTo,
provenance: artefact.provenance
}).catch((error) => {
console.error("Third-party deletion push failed:", error);
})
)
);

Comment thread libs/legacy-third-party-fulfilment/src/push/headers.test.ts Outdated
Comment on lines +58 to +80
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
// Drain the response body to free the socket
res.resume();

res.on("end", () => {
const statusCode = res.statusCode ?? 0;
resolve({ statusCode, success: isSuccessStatus(statusCode) });
});
});

req.on("timeout", () => {
req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
});

req.on("error", reject);

if (bodyBuffer.byteLength > 0) {
req.write(bodyBuffer);
}

req.end();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing res error handler can reject after resolve.

If the response stream emits error after headers are received (e.g. socket reset mid-drain), there's no res.on("error", ...) listener, so the promise may never settle (the end event won't fire) and the retry layer will hang until the outer request timeout fires — but req.destroy inside the timeout handler won't necessarily re-emit on req once the response has started. Adding res.on("error", reject) closes this gap cheaply.

🛡️ Suggested fix
     const req = https.request(options, (res) => {
       // Drain the response body to free the socket
       res.resume();
 
+      res.on("error", reject);
       res.on("end", () => {
         const statusCode = res.statusCode ?? 0;
         resolve({ statusCode, success: isSuccessStatus(statusCode) });
       });
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
// Drain the response body to free the socket
res.resume();
res.on("end", () => {
const statusCode = res.statusCode ?? 0;
resolve({ statusCode, success: isSuccessStatus(statusCode) });
});
});
req.on("timeout", () => {
req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
});
req.on("error", reject);
if (bodyBuffer.byteLength > 0) {
req.write(bodyBuffer);
}
req.end();
});
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
// Drain the response body to free the socket
res.resume();
res.on("error", reject);
res.on("end", () => {
const statusCode = res.statusCode ?? 0;
resolve({ statusCode, success: isSuccessStatus(statusCode) });
});
});
req.on("timeout", () => {
req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
});
req.on("error", reject);
if (bodyBuffer.byteLength > 0) {
req.write(bodyBuffer);
}
req.end();
});

Comment on lines +47 to +54
it("treats unknown sensitivity as PUBLIC and includes all levels", async () => {
await findSubscribersByListType(42, "UNKNOWN");

expect(mockFindMany).toHaveBeenCalledWith({
where: { listTypeId: 42, sensitivity: { in: ["PUBLIC", "PRIVATE", "CLASSIFIED"] } },
include: { user: true }
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test encodes the fail-open behaviour.

Paired with the eligibleSensitivities comment above: once unknown sensitivities fail closed, this case should assert an empty set (or an error) rather than broadcasting to all levels. Update alongside the production fix.

Comment on lines +6 to +10
function eligibleSensitivities(publicationSensitivity: string): SensitivityLevel[] {
const rank = SENSITIVITY_ORDER.indexOf(publicationSensitivity as SensitivityLevel);
const minRank = rank === -1 ? 0 : rank;
return SENSITIVITY_ORDER.filter((_, i) => i >= minRank);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unknown sensitivity silently falls open to PUBLIC.

Defaulting an unrecognised publicationSensitivity to rank 0 means an artefact with a typo or a future-added level (e.g. "RESTRICTED") would be broadcast to every subscriber, including PUBLIC. For a classification-driven distribution this is a fail-open posture. Prefer to fail closed (return [] / throw) and log, so mis-tagged publications are not leaked.

🛡️ Suggested fix
 function eligibleSensitivities(publicationSensitivity: string): SensitivityLevel[] {
   const rank = SENSITIVITY_ORDER.indexOf(publicationSensitivity as SensitivityLevel);
-  const minRank = rank === -1 ? 0 : rank;
-  return SENSITIVITY_ORDER.filter((_, i) => i >= minRank);
+  if (rank === -1) {
+    console.error(`[ThirdParty] Unknown sensitivity "${publicationSensitivity}" — refusing to distribute`);
+    return [];
+  }
+  return SENSITIVITY_ORDER.slice(rank);
 }

Comment on lines +46 to +53
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(pushWithRetry).mockResolvedValue({ statusCode: 200, success: true });
vi.mocked(findSubscribersByListType).mockResolvedValue([{ id: "sub-1", user: { id: "user-1", name: "courtel" } }] as never);
mockPushLogCreate.mockResolvedValue({});
process.env.COURTEL_API_URL = "https://courtel.example.com/api";
process.env.COURTEL_CERTIFICATE = Buffer.from("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----").toString("base64");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does vitest vi.resetAllMocks reset implementations set via vi.mock factory mockReturnValue?

💡 Result:

Yes, vi.resetAllMocks resets implementations set via mockReturnValue on mock functions created in a vi.mock factory. It calls .mockReset on all spies/mocks, which clears call history and resets the mock implementation to its default (empty function returning undefined for vi.fn without initial impl, or original impl for vi.fn(impl)). Any mockReturnValue calls are overrides on the mock and get reset. For example, in a vi.mock('./module', => ({ fn: vi.fn.mockReturnValue('value') })), calling vi.resetAllMocks will reset fn so it no longer returns 'value' but undefined. This is evidenced by Vitest docs stating vi.resetAllMocks "Calls .mockReset on all spies" (sources 2,3,7,9), .mockReset resets implementation (source 8), and real-world cases where mockReset: true config (auto resetAllMocks) resets factory-set implementations, requiring per-test re-setup (source 6). Note: Module-level vi.mock itself persists (hoisted, cached); resetAllMocks affects mock functions within, not the module mock. Use vi.doUnmock + reimport to fully reset module mocks.

Citations:


vi.resetAllMocks() clears factory-mocked implementations.

This call resets the mock implementations set inside vi.mock(...) factories, so buildPushHeaders and getLocationWithDetails (lines 7 and 19) return undefined after reset. The current tests don't depend on these values, but this is fragile—future tests inspecting the headers will silently get undefined. Consider using vi.clearAllMocks() instead and re-arm only the mocks you intentionally need per test.

return;
}

const location = await getLocationWithDetails(Number.parseInt(locationId, 10));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against non-numeric locationId.

Number.parseInt(locationId, 10) will silently produce NaN for malformed inputs, which then flows into getLocationWithDetails and yields null — the push still goes out with empty location headers. Consider validating and logging/skipping when the parsed id is NaN, so bad upstream data is surfaced rather than masked.

Comment on lines +78 to +80
writePushLog(artefactId, listTypeId, isUpdate ? "UPDATE" : "CREATE", result).catch((err) => {
console.warn(`${logPrefix} Failed to write push log:`, err instanceof Error ? err.message : String(err));
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fire-and-forget writePushLog can lose log rows on short-lived processes.

The log write is intentionally not awaited, but callers that invoke sendThirdPartyPublications/sendThirdPartyDeletion in fire-and-forget style from request handlers or the deletion flow may return/exit before the Prisma write resolves, in which case the push-log row is dropped and only the .catch warning remains — and only if the process is still alive. Since the push itself is already awaited, awaiting the log write as well would be cheap and materially more reliable.

Proposed change
-  const result = await pushWithRetry(secrets.url, secrets.certPem, headers, body, logPrefix, pdfPath);
-  writePushLog(artefactId, listTypeId, isUpdate ? "UPDATE" : "CREATE", result).catch((err) => {
-    console.warn(`${logPrefix} Failed to write push log:`, err instanceof Error ? err.message : String(err));
-  });
+  const result = await pushWithRetry(secrets.url, secrets.certPem, headers, body, logPrefix, pdfPath);
+  try {
+    await writePushLog(artefactId, listTypeId, isUpdate ? "UPDATE" : "CREATE", result);
+  } catch (err) {
+    console.warn(`${logPrefix} Failed to write push log:`, err instanceof Error ? err.message : String(err));
+  }

Also applies to: 100-102

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
libs/publication/src/processing/service.ts (1)

186-186: Rename the new boolean flag to an is/has/can form.

skipThirdPartyPush is readable, but it diverges from the repo’s boolean naming rule (for example, isThirdPartyPushSkipped).

As per coding guidelines, "Booleans should use is/has/can prefixes (e.g., isActive, hasAccess, canEdit)."

Also applies to: 214-214


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 98a7e3f0-f985-4674-8c01-0702c68535a4

📥 Commits

Reviewing files that changed from the base of the PR and between 6517344 and 5796d1a.

📒 Files selected for processing (11)
  • libs/admin-pages/src/manual-upload/file-storage.ts
  • libs/admin-pages/src/pages/manual-upload-summary/index.ts
  • libs/api/src/blob-ingestion/file-storage.ts
  • libs/legacy-third-party-fulfilment/src/push/http-client.test.ts
  • libs/legacy-third-party-fulfilment/src/push/http-client.ts
  • libs/legacy-third-party-fulfilment/src/push/retry.test.ts
  • libs/legacy-third-party-fulfilment/src/push/retry.ts
  • libs/legacy-third-party-fulfilment/src/service.test.ts
  • libs/legacy-third-party-fulfilment/src/service.ts
  • libs/publication/src/processing/service.test.ts
  • libs/publication/src/processing/service.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • libs/admin-pages/src/pages/manual-upload-summary/index.ts
  • libs/publication/src/processing/service.test.ts
  • libs/legacy-third-party-fulfilment/src/push/http-client.ts
  • libs/legacy-third-party-fulfilment/src/service.test.ts
  • libs/legacy-third-party-fulfilment/src/push/http-client.test.ts

Comment on lines +25 to +136
it("returns success immediately without retrying on first attempt", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 200, success: true });

const result = await pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);

expect(result).toEqual({ statusCode: 200, success: true });
expect(mockExecutePush).toHaveBeenCalledTimes(1);
});

it("retries on 5xx and returns success on the second attempt", async () => {
mockExecutePush.mockResolvedValueOnce({ statusCode: 503, success: false }).mockResolvedValueOnce({ statusCode: 200, success: true });

const resultPromise = pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);
await vi.runAllTimersAsync();
const result = await resultPromise;

expect(result).toEqual({ statusCode: 200, success: true });
expect(mockExecutePush).toHaveBeenCalledTimes(2);
});

it("does not retry on 4xx (except 429) and returns the 4xx result", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 400, success: false });

const result = await pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);

expect(result).toEqual({ statusCode: 400, success: false });
expect(mockExecutePush).toHaveBeenCalledTimes(1);
});

it("does not retry on 401", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 401, success: false });

const result = await pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);

expect(result).toEqual({ statusCode: 401, success: false });
expect(mockExecutePush).toHaveBeenCalledTimes(1);
});

it("retries on 429 rate-limit response", async () => {
mockExecutePush.mockResolvedValueOnce({ statusCode: 429, success: false }).mockResolvedValueOnce({ statusCode: 200, success: true });

const resultPromise = pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);
await vi.runAllTimersAsync();
const result = await resultPromise;

expect(result).toEqual({ statusCode: 200, success: true });
expect(mockExecutePush).toHaveBeenCalledTimes(2);
});

it("exhausts all 3 attempts on repeated 5xx and returns the last result", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 502, success: false });

const resultPromise = pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);
await vi.runAllTimersAsync();
const result = await resultPromise;

expect(result).toEqual({ statusCode: 502, success: false });
expect(mockExecutePush).toHaveBeenCalledTimes(3);
});

it("retries and succeeds on the third attempt", async () => {
mockExecutePush
.mockResolvedValueOnce({ statusCode: 500, success: false })
.mockResolvedValueOnce({ statusCode: 500, success: false })
.mockResolvedValueOnce({ statusCode: 201, success: true });

const resultPromise = pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);
await vi.runAllTimersAsync();
const result = await resultPromise;

expect(result).toEqual({ statusCode: 201, success: true });
expect(mockExecutePush).toHaveBeenCalledTimes(3);
});

it("retries on network error (rejected promise) and returns success on second attempt", async () => {
mockExecutePush.mockRejectedValueOnce(new Error("ECONNREFUSED")).mockResolvedValueOnce({ statusCode: 200, success: true });

const resultPromise = pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY);
await vi.runAllTimersAsync();
const result = await resultPromise;

expect(result).toEqual({ statusCode: 200, success: true });
expect(mockExecutePush).toHaveBeenCalledTimes(2);
});

it("passes through null body correctly", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 204, success: true });

const result = await pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, null);

expect(result).toEqual({ statusCode: 204, success: true });
expect(mockExecutePush).toHaveBeenCalledWith(TEST_URL, TEST_CERT, TEST_HEADERS, null, undefined, undefined);
});

it("forwards pdfPath to executePush", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 200, success: true });
const pdfPath = "/tmp/publication.pdf";

await pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY, "[ThirdParty]", pdfPath);

expect(mockExecutePush).toHaveBeenCalledWith(TEST_URL, TEST_CERT, TEST_HEADERS, TEST_BODY, pdfPath, undefined);
});

it("forwards flatFilePath to executePush", async () => {
mockExecutePush.mockResolvedValue({ statusCode: 200, success: true });
const flatFilePath = "/tmp/artefact-1.xlsx";

await pushWithRetry(TEST_URL, TEST_CERT, TEST_HEADERS, null, "[ThirdParty]", undefined, flatFilePath);

expect(mockExecutePush).toHaveBeenCalledWith(TEST_URL, TEST_CERT, TEST_HEADERS, null, undefined, flatFilePath);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Please add a 204-without-Location negative test.

The suite currently proves 204 success paths, but it does not protect the required behaviour where a bare 204 must not be accepted as a valid acknowledgement.

Comment on lines +44 to +45
if (SUCCESS_STATUSES.includes(lastResult.statusCode)) {
return lastResult;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

204 responses are accepted too broadly.

At Line 44, any 204 is treated as success, but the fulfilment contract requires 204 only when a Location header is present. Please gate 204 success on that header (which likely needs executePush to return header metadata).

Comment on lines +209 to +210
sensitivity = "",
language = "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid empty-string defaults for sensitivity and language.

If callers omit these fields, subscriber matching and outbound header values can silently degrade, causing missed pushes or invalid metadata.

Also applies to: 260-261

Comment on lines +262 to +263
displayFrom: displayFrom ?? new Date(),
displayTo: displayTo ?? new Date(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

displayFrom/displayTo should not default to new Date().

This produces incorrect x-display-from/x-display-to values when publication window dates are missing, which can misrepresent list validity.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

…lment

# Conflicts:
#	apps/api/helm/values.yaml
#	libs/admin-pages/src/pages/manual-upload-summary/index.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

…lment

# Conflicts:
#	libs/postgres-prisma/src/schema-discovery.test.ts
#	libs/postgres-prisma/src/schema-discovery.ts
#	libs/publication/src/repository/queries.ts
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

…arty-subscription-fulfilment

# Conflicts:
#	libs/api/src/blob-ingestion/repository/service.test.ts
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

@sonarqubecloud

sonarqubecloud Bot commented Jun 5, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment Successful 🚀

Your preview environment is ready:

The environment will be automatically cleaned up when this PR is closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[VIBE-367] Third Party subscription Fulfilment - Current

5 participants