diff --git a/lib/services/pax8-sync-service.test.ts b/lib/services/pax8-sync-service.test.ts index 1dcb2b1..79766fb 100644 --- a/lib/services/pax8-sync-service.test.ts +++ b/lib/services/pax8-sync-service.test.ts @@ -194,7 +194,11 @@ describe('Pax8SyncService — syncOrders + syncCompanyMatches', () => { expect(matchEntity).toBeDefined(); expect(matchEntity!.success).toBe(true); expect(matchEntity!.upserted).toBe(1); // autoLinked - expect(matchEntity!.tombstoned).toBe(1); // flaggedAmbiguous + flaggedNoCandidate + // No soft-deletes are performed by matching (CR-02) — tombstoned must + // always be 0 so sync_history.records_deleted never fabricates a + // "deleted" count for review-flagged companies. + expect(matchEntity!.tombstoned).toBe(0); + expect(matchEntity!.flaggedForReview).toBe(1); // flaggedAmbiguous + flaggedNoCandidate }); it("fullSync's entities array includes an 'orders' result and a 'company_matches' result", async () => { diff --git a/lib/services/pax8-sync-service.ts b/lib/services/pax8-sync-service.ts index 89bd71d..d322744 100644 --- a/lib/services/pax8-sync-service.ts +++ b/lib/services/pax8-sync-service.ts @@ -108,12 +108,13 @@ export class Pax8SyncService { const status: 'completed' | 'failed' = success ? 'completed' : 'failed'; const totalUpserted = entities.reduce((sum, e) => sum + e.upserted, 0); const totalTombstoned = entities.reduce((sum, e) => sum + e.tombstoned, 0); + const totalFlaggedForReview = entities.reduce((sum, e) => sum + (e.flaggedForReview ?? 0), 0); const errors = entities.filter(e => e.error).map(e => `${e.entity}: ${e.error}`); await this.updateHistory(startedAt, status, totalUpserted, totalTombstoned, errors.join('; ') || null); console.log( - `[Pax8Sync] Full sync ${status} (${syncId}) — ${totalUpserted} upserted, ${totalTombstoned} tombstoned` + `[Pax8Sync] Full sync ${status} (${syncId}) — ${totalUpserted} upserted, ${totalTombstoned} tombstoned, ${totalFlaggedForReview} flagged for review` ); return { @@ -520,14 +521,21 @@ export class Pax8SyncService { const start = Date.now(); try { const result = await matchPax8Companies(); + const flaggedForReview = result.flaggedAmbiguous + result.flaggedNoCandidate; + if (flaggedForReview > 0) { + console.log(`[Pax8Sync] Company match: ${flaggedForReview} compan${flaggedForReview === 1 ? 'y' : 'ies'} flagged for manual review (${result.flaggedAmbiguous} ambiguous, ${result.flaggedNoCandidate} no-candidate)`); + } return { entity: 'company_matches', success: true, upserted: result.autoLinked, - // Repurposed: not a soft-delete tombstone count — the number of - // pax8_companies rows flagged for manual review this run (ambiguous - // + no-candidate), surfaced through the same rollup field. - tombstoned: result.flaggedAmbiguous + result.flaggedNoCandidate, + // No soft-deletes are ever performed by matching — real deletion + // counts belong in `tombstoned`/sync_history.records_deleted, and + // conflating the two fabricates "deleted" counts in the admin Sync + // History UI (CR-02). Review-flag counts are reported separately via + // `flaggedForReview`. + tombstoned: 0, + flaggedForReview, durationMs: result.durationMs, }; } catch (err) { diff --git a/lib/types/pax8.ts b/lib/types/pax8.ts index f626ce6..8da3338 100644 --- a/lib/types/pax8.ts +++ b/lib/types/pax8.ts @@ -120,6 +120,12 @@ export interface Pax8EntitySyncResult { tombstoned: number; durationMs: number; error?: string; + // Populated only by the 'company_matches' entity: pax8_companies rows + // flagged for manual review this run (ambiguous + no-candidate). Kept + // separate from `tombstoned` (a real soft-delete count) so it is never + // folded into sync_history.records_deleted / the Sync History UI's + // "deleted" column. + flaggedForReview?: number; } export interface Pax8SyncResult {