108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { identityApp, type IdentityApp } from './helpers/identity-app';
|
|
import { checkoutFixture } from './helpers/checkout';
|
|
import * as audit from '../src/identity/audit';
|
|
|
|
describe('checkout allocation and final-write rollback', () => {
|
|
let ctx: IdentityApp;
|
|
beforeAll(async () => {
|
|
ctx = await identityApp();
|
|
}, 60000);
|
|
afterAll(async () => {
|
|
await ctx.close();
|
|
});
|
|
beforeEach(async () => {
|
|
await ctx.clearLimits();
|
|
});
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
it('rolls back order, holds and cart clearing when the audit write fails', async () => {
|
|
const f = await checkoutFixture(ctx);
|
|
jest
|
|
.spyOn(audit, 'recordAudit')
|
|
.mockRejectedValueOnce(new Error('Synthetic audit failure'));
|
|
const result = await ctx
|
|
.api()
|
|
.post('/api/v1/checkout')
|
|
.auth(f.actor.token, { type: 'bearer' })
|
|
.send(f.input)
|
|
.expect(500);
|
|
expect(result.body.code).toBe('INTERNAL_FAILURE');
|
|
expect(JSON.stringify(result.body)).not.toContain('Synthetic');
|
|
expect(
|
|
await ctx.db.order.count({ where: { userId: f.actor.userId } }),
|
|
).toBe(0);
|
|
expect(
|
|
await ctx.db.stockReservation.count({
|
|
where: { userId: f.actor.userId },
|
|
}),
|
|
).toBe(0);
|
|
const cart = await ctx
|
|
.api()
|
|
.get('/api/v1/cart')
|
|
.auth(f.actor.token, { type: 'bearer' })
|
|
.expect(200);
|
|
expect(cart.body.version).toBe(1);
|
|
expect(cart.body.lines).toHaveLength(1);
|
|
await ctx
|
|
.api()
|
|
.post('/api/v1/checkout')
|
|
.auth(f.actor.token, { type: 'bearer' })
|
|
.send(f.input)
|
|
.expect(201);
|
|
});
|
|
it('splits a line across warehouses without consuming on-hand stock', async () => {
|
|
const f = await checkoutFixture(ctx, 1);
|
|
const warehouse = await ctx.db.warehouse.create({
|
|
data: { organizationId: f.actor.organizationId, name: randomUUID() },
|
|
});
|
|
const stock = await ctx.db.stockItem.create({
|
|
data: {
|
|
organizationId: f.actor.organizationId,
|
|
warehouseId: warehouse.id,
|
|
variantId: f.variant.id,
|
|
},
|
|
});
|
|
await ctx
|
|
.api()
|
|
.post('/api/v1/inventory/adjustments')
|
|
.auth(ctx.token, { type: 'bearer' })
|
|
.send({
|
|
stockItemId: stock.id,
|
|
delta: 1,
|
|
reason: 'Opening balance',
|
|
idempotencyKey: randomUUID(),
|
|
})
|
|
.expect(201);
|
|
const result = await ctx
|
|
.api()
|
|
.post('/api/v1/checkout')
|
|
.auth(f.actor.token, { type: 'bearer' })
|
|
.send(f.input)
|
|
.expect(201);
|
|
const holds = await ctx.db.stockReservation.findMany({
|
|
where: { orderId: result.body.id },
|
|
});
|
|
expect(holds).toHaveLength(2);
|
|
expect(holds.map((hold) => hold.quantity)).toEqual([1, 1]);
|
|
});
|
|
it('revalidates product availability and currency when checking out a saved cart', async () => {
|
|
const f = await checkoutFixture(ctx);
|
|
await ctx.db.productVariant.update({
|
|
where: { id: f.variant.id },
|
|
data: { active: false },
|
|
});
|
|
const response = await ctx
|
|
.api()
|
|
.post('/api/v1/checkout')
|
|
.auth(f.actor.token, { type: 'bearer' })
|
|
.send(f.input)
|
|
.expect(409);
|
|
expect(response.body.code).toBe('CART_ITEM_UNAVAILABLE');
|
|
expect(
|
|
await ctx.db.order.count({ where: { userId: f.actor.userId } }),
|
|
).toBe(0);
|
|
});
|
|
});
|