feat(berths): CM-2 — bulk price-reconcile service (parse + apply)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
86
tests/integration/berth-price-reconcile.test.ts
Normal file
86
tests/integration/berth-price-reconcile.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Integration tests for the bulk berth price-reconcile service (CM-2 Part A).
|
||||
*
|
||||
* Uses the real filesystem storage backend (seeded below) + a real spec-sheet
|
||||
* PDF, so the full upload → store → re-parse → extract path is exercised end to
|
||||
* end with no storage mock.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
listPriceReconciliation,
|
||||
applyBulkBerthPrices,
|
||||
} from '@/lib/services/berth-price-reconcile.service';
|
||||
import { uploadBerthPdf } from '@/lib/services/berth-pdf.service';
|
||||
import { db } from '@/lib/db';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
import { systemSettings } from '@/lib/db/schema/system';
|
||||
|
||||
import { makeBerth, makePort } from '../helpers/factories';
|
||||
|
||||
const A1_PDF = readFileSync(path.join(process.cwd(), 'berth_pdf_example/Berth_Spec_Sheet_A1.pdf'));
|
||||
|
||||
beforeEach(async () => {
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({ key: 'storage_backend', value: 'filesystem', portId: null, updatedBy: null })
|
||||
.onConflictDoNothing();
|
||||
});
|
||||
|
||||
describe('listPriceReconciliation', () => {
|
||||
it('parses the main price for a berth with a PDF and flags one without', async () => {
|
||||
const port = await makePort();
|
||||
const withPdf = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'A1' } });
|
||||
const withoutPdf = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'Z9' } });
|
||||
|
||||
await uploadBerthPdf({
|
||||
berthId: withPdf.id,
|
||||
portId: port.id,
|
||||
buffer: A1_PDF,
|
||||
fileName: 'Berth_Spec_Sheet_A1.pdf',
|
||||
uploadedBy: 'test-user',
|
||||
});
|
||||
|
||||
const rows = await listPriceReconciliation(port.id);
|
||||
const w = rows.find((r) => r.mooringNumber === 'A1');
|
||||
const wo = rows.find((r) => r.mooringNumber === 'Z9');
|
||||
|
||||
expect(w?.parsedPrice).toBe(3880800);
|
||||
expect(w?.parsedCurrency).toBe('USD');
|
||||
expect(w?.currentPrice).toBeNull();
|
||||
expect(w?.status).toBe('changed'); // CRM price null → changed
|
||||
expect(wo?.status).toBe('no_pdf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyBulkBerthPrices', () => {
|
||||
it('writes only approved, in-port berths and skips cross-port ids', async () => {
|
||||
const portA = await makePort();
|
||||
const portB = await makePort();
|
||||
const berthA = await makeBerth({ portId: portA.id, overrides: { mooringNumber: 'A1' } });
|
||||
const berthB = await makeBerth({ portId: portB.id, overrides: { mooringNumber: 'A1' } });
|
||||
|
||||
const res = await applyBulkBerthPrices(
|
||||
portA.id,
|
||||
[
|
||||
{ berthId: berthA.id, price: 3880800, currency: 'USD' },
|
||||
{ berthId: berthB.id, price: 999, currency: 'USD' }, // foreign port → skipped
|
||||
],
|
||||
'test-user',
|
||||
);
|
||||
|
||||
expect(res.updated).toBe(1);
|
||||
|
||||
const [a] = await db.select().from(berths).where(eq(berths.id, berthA.id));
|
||||
expect(Number(a!.price)).toBe(3880800);
|
||||
expect(a!.priceCurrency).toBe('USD');
|
||||
|
||||
const [b] = await db.select().from(berths).where(eq(berths.id, berthB.id));
|
||||
expect(b!.price).toBeNull(); // untouched
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user