Files
pn-new-crm/tests/integration/berth-price-reconcile.test.ts

130 lines
4.7 KiB
TypeScript
Raw Normal View History

/**
* 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, makeFullPermissions, makePort } from '../helpers/factories';
import { makeMockCtx, makeMockRequest } from '../helpers/route-tester';
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' } });
// No-PDF berth — created for its 'no_pdf' row; the value isn't referenced.
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
});
});
describe('price-reconcile route handlers', () => {
it('GET lists rows and POST apply writes the approved price', async () => {
const { getHandler } = await import('@/app/api/v1/berths/price-reconcile/handlers');
const { postHandler } = await import('@/app/api/v1/berths/price-reconcile/apply/handlers');
const port = await makePort();
const berth = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'A1' } });
await uploadBerthPdf({
berthId: berth.id,
portId: port.id,
buffer: A1_PDF,
fileName: 'Berth_Spec_Sheet_A1.pdf',
uploadedBy: 'test-user',
});
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const listRes = await getHandler(
makeMockRequest('GET', 'http://t/api/v1/berths/price-reconcile'),
ctx,
{},
);
const listJson = (await listRes.json()) as {
data: Array<{ mooringNumber: string; parsedPrice: number | null }>;
};
expect(listJson.data.find((r) => r.mooringNumber === 'A1')?.parsedPrice).toBe(3880800);
const applyRes = await postHandler(
makeMockRequest('POST', 'http://t/api/v1/berths/price-reconcile/apply', {
body: { approvals: [{ berthId: berth.id, price: 3880800, currency: 'USD' }] },
}),
ctx,
{},
);
const applyJson = (await applyRes.json()) as { data: { updated: number } };
expect(applyJson.data.updated).toBe(1);
const [b] = await db.select().from(berths).where(eq(berths.id, berth.id));
expect(Number(b!.price)).toBe(3880800);
});
});