import { Injectable } from '@nestjs/common'; import { AccessStore } from '../identity/access.store'; import type { Principal } from '../identity/identity.types'; import { recordAudit } from '../identity/audit'; import { AppError } from '../common/errors/app-error'; import { assertReplay, commandHash } from '../inventory/inventory.policy'; import type { CheckoutInput } from './checkout.schemas'; import { checkoutSnapshot } from './checkout-snapshot'; import { holdOrderStock } from './stock-allocation'; import { orderView } from './order-view'; import { snapshotPrice } from '../pricing/snapshot-price'; import { enqueueEvent } from '../events/enqueue-event'; @Injectable() export class CheckoutStore { constructor(private readonly access: AccessStore) {} create(actor: Principal, input: CheckoutInput) { const requestHash = commandHash( input.cartVersion, input.addressId, input.couponCode ?? '', ); // The organization lock also serializes catalog, coupon and cart changes. // Stock locks below coordinate with independent inventory transactions. return this.access.mutate(actor, null, async (tx) => { const previous = await tx.order.findUnique({ where: { userId_organizationId_idempotencyKey: { userId: actor.userId, organizationId: actor.organizationId, idempotencyKey: input.idempotencyKey, }, }, include: { lines: true, pricing: true }, }); if (previous) { assertReplay(previous.requestHash, requestHash); return orderView(previous); } const now = new Date(); if ( (await tx.order.count({ where: { userId: actor.userId, organizationId: actor.organizationId, status: 'PENDING_PAYMENT', expiresAt: { gt: now }, }, })) >= 10 ) throw new AppError('ORDER_LIMIT'); const { cartId, lines, ...snapshot } = await checkoutSnapshot( tx, actor, input, ); const expiresAt = new Date(now.getTime() + 15 * 60000); const order = await tx.order.create({ data: { ...snapshot, organizationId: actor.organizationId, userId: actor.userId, idempotencyKey: input.idempotencyKey, requestHash, expiresAt, lines: { create: lines }, }, include: { lines: true }, }); await holdOrderStock(tx, actor, order.id, expiresAt, lines); const pricing = await snapshotPrice(tx, order); await enqueueEvent(tx, actor.organizationId, order.id, 'order.created'); await tx.cartLine.deleteMany({ where: { cartId } }); await tx.cart.update({ where: { id: cartId }, data: { version: { increment: 1 } }, }); await recordAudit( tx, actor.organizationId, actor.userId, 'order.created', order.id, ); return orderView({ ...order, pricing }); }); } }