@open-mercato/core 0.6.7-develop.6569.1.c06dee4866 → 0.6.7-develop.6573.1.28649ddec6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/gateway_payment_operation/index.js +35 -0
  3. package/dist/generated/entities/gateway_payment_operation/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +1 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +18 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/directory/commands/organizations.js +1 -1
  9. package/dist/modules/directory/commands/organizations.js.map +2 -2
  10. package/dist/modules/payment_gateways/api/cancel/route.js +2 -1
  11. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  12. package/dist/modules/payment_gateways/api/capture/route.js +2 -1
  13. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  14. package/dist/modules/payment_gateways/api/refund/route.js +2 -1
  15. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  16. package/dist/modules/payment_gateways/data/entities.js +67 -0
  17. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  18. package/dist/modules/payment_gateways/data/validators.js +6 -3
  19. package/dist/modules/payment_gateways/data/validators.js.map +2 -2
  20. package/dist/modules/payment_gateways/lib/gateway-service.js +145 -111
  21. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  22. package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js +183 -0
  23. package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js.map +7 -0
  24. package/dist/modules/payment_gateways/migrations/Migration20260709220735_payment_gateways.js +13 -0
  25. package/dist/modules/payment_gateways/migrations/Migration20260709220735_payment_gateways.js.map +7 -0
  26. package/dist/modules/sales/api/quotes/accept/route.js +1 -1
  27. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  28. package/dist/modules/sales/api/quotes/public/[token]/route.js +0 -3
  29. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  30. package/generated/entities/gateway_payment_operation/index.ts +16 -0
  31. package/generated/entities.ids.generated.ts +1 -0
  32. package/generated/entity-fields-registry.ts +18 -0
  33. package/package.json +7 -7
  34. package/src/modules/directory/commands/organizations.ts +1 -1
  35. package/src/modules/payment_gateways/api/cancel/route.ts +1 -0
  36. package/src/modules/payment_gateways/api/capture/route.ts +1 -0
  37. package/src/modules/payment_gateways/api/refund/route.ts +1 -0
  38. package/src/modules/payment_gateways/data/entities.ts +59 -0
  39. package/src/modules/payment_gateways/data/validators.ts +3 -0
  40. package/src/modules/payment_gateways/lib/gateway-service.ts +168 -117
  41. package/src/modules/payment_gateways/lib/payment-operation-idempotency.ts +238 -0
  42. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +331 -1
  43. package/src/modules/payment_gateways/migrations/Migration20260709220735_payment_gateways.ts +12 -0
  44. package/src/modules/sales/api/quotes/accept/route.ts +1 -1
  45. package/src/modules/sales/api/quotes/public/[token]/route.ts +4 -9
@@ -0,0 +1,183 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { CrudHttpError, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
3
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
+ import { GatewayPaymentOperation } from "../data/entities.js";
5
+ const OPERATION_LEASE_MS = 10 * 60 * 1e3;
6
+ const OPERATION_UNIQUE_CONSTRAINT = "gateway_payment_operations_scope_operation_unique";
7
+ function digest(value) {
8
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
9
+ }
10
+ function operationConflict(code, operationId, message) {
11
+ return new CrudHttpError(409, { error: message, code, operationId });
12
+ }
13
+ async function findOperation(em, operationId, scope) {
14
+ return findOneWithDecryption(
15
+ em,
16
+ GatewayPaymentOperation,
17
+ {
18
+ operationId,
19
+ organizationId: scope.organizationId,
20
+ tenantId: scope.tenantId
21
+ },
22
+ void 0,
23
+ scope
24
+ );
25
+ }
26
+ function buildOperationIdentity(input) {
27
+ const requestHash = digest({
28
+ transactionId: input.transactionId,
29
+ providerKey: input.providerKey,
30
+ action: input.action,
31
+ payload: input.payload
32
+ });
33
+ const operationId = input.operationId ?? `auto_${requestHash}`;
34
+ const providerIdempotencyKey = `om-pg-${digest({
35
+ tenantId: input.scope.tenantId,
36
+ organizationId: input.scope.organizationId,
37
+ transactionId: input.transactionId,
38
+ action: input.action,
39
+ operationId
40
+ })}`;
41
+ return { operationId, providerIdempotencyKey, requestHash };
42
+ }
43
+ function claimFields(now) {
44
+ return {
45
+ status: "in_progress",
46
+ attemptToken: randomUUID(),
47
+ leaseExpiresAt: new Date(now.getTime() + OPERATION_LEASE_MS),
48
+ updatedAt: now
49
+ };
50
+ }
51
+ async function resolveExistingOperation(em, existing, identity) {
52
+ if (existing.requestHash !== identity.requestHash) {
53
+ throw operationConflict(
54
+ "payment_operation_conflict",
55
+ identity.operationId,
56
+ "Payment operation id was already used with a different request"
57
+ );
58
+ }
59
+ if (existing.providerIdempotencyKey !== identity.providerIdempotencyKey) {
60
+ throw operationConflict(
61
+ "payment_operation_conflict",
62
+ identity.operationId,
63
+ "Payment operation id resolved to a different provider request"
64
+ );
65
+ }
66
+ if (existing.status === "succeeded" && existing.result) {
67
+ return { kind: "completed", result: existing.result };
68
+ }
69
+ const now = /* @__PURE__ */ new Date();
70
+ const stale = existing.status === "in_progress" && existing.leaseExpiresAt instanceof Date && existing.leaseExpiresAt <= now;
71
+ if (existing.status !== "failed" && !stale) {
72
+ throw operationConflict(
73
+ "payment_operation_in_progress",
74
+ identity.operationId,
75
+ "Payment operation is already in progress"
76
+ );
77
+ }
78
+ const next = claimFields(now);
79
+ const where = existing.status === "failed" ? { id: existing.id, status: "failed", attemptToken: existing.attemptToken } : { id: existing.id, status: "in_progress", attemptToken: existing.attemptToken, leaseExpiresAt: { $lt: now } };
80
+ const claimed = await em.nativeUpdate(
81
+ GatewayPaymentOperation,
82
+ where,
83
+ { ...next, attemptCount: existing.attemptCount + 1 }
84
+ );
85
+ if (claimed !== 1) {
86
+ throw operationConflict(
87
+ "payment_operation_in_progress",
88
+ identity.operationId,
89
+ "Payment operation is already in progress"
90
+ );
91
+ }
92
+ Object.assign(existing, next, { attemptCount: existing.attemptCount + 1 });
93
+ return {
94
+ kind: "claimed",
95
+ claim: {
96
+ record: existing,
97
+ attemptToken: next.attemptToken,
98
+ providerIdempotencyKey: existing.providerIdempotencyKey
99
+ }
100
+ };
101
+ }
102
+ async function preparePaymentOperation(input) {
103
+ const identity = buildOperationIdentity(input);
104
+ const existing = await findOperation(input.em, identity.operationId, input.scope);
105
+ if (existing) {
106
+ return resolveExistingOperation(input.em, existing, identity);
107
+ }
108
+ input.assertInitialAllowed();
109
+ const now = /* @__PURE__ */ new Date();
110
+ const claim = claimFields(now);
111
+ const record = input.em.create(GatewayPaymentOperation, {
112
+ operationId: identity.operationId,
113
+ transactionId: input.transactionId,
114
+ operationType: input.action,
115
+ providerKey: input.providerKey,
116
+ requestHash: identity.requestHash,
117
+ providerIdempotencyKey: identity.providerIdempotencyKey,
118
+ status: claim.status,
119
+ attemptToken: claim.attemptToken,
120
+ attemptCount: 1,
121
+ result: null,
122
+ leaseExpiresAt: claim.leaseExpiresAt,
123
+ organizationId: input.scope.organizationId,
124
+ tenantId: input.scope.tenantId,
125
+ createdAt: now,
126
+ updatedAt: now
127
+ });
128
+ try {
129
+ await input.em.persist(record).flush();
130
+ return {
131
+ kind: "claimed",
132
+ claim: {
133
+ record,
134
+ attemptToken: claim.attemptToken,
135
+ providerIdempotencyKey: identity.providerIdempotencyKey
136
+ }
137
+ };
138
+ } catch (error) {
139
+ if (!isUniqueViolation(error, OPERATION_UNIQUE_CONSTRAINT) && !isUniqueViolation(error)) {
140
+ throw error;
141
+ }
142
+ const winner = await findOperation(input.em, identity.operationId, input.scope);
143
+ if (!winner) throw error;
144
+ return resolveExistingOperation(input.em, winner, identity);
145
+ }
146
+ }
147
+ async function completePaymentOperation(em, claim, result, applyResult) {
148
+ const statusChanged = await em.transactional(async (tx) => {
149
+ const completed = await tx.nativeUpdate(
150
+ GatewayPaymentOperation,
151
+ { id: claim.record.id, status: "in_progress", attemptToken: claim.attemptToken },
152
+ { status: "succeeded", result, leaseExpiresAt: null, updatedAt: /* @__PURE__ */ new Date() }
153
+ );
154
+ if (completed !== 1) {
155
+ throw operationConflict(
156
+ "payment_operation_claim_lost",
157
+ claim.record.operationId,
158
+ "Payment operation claim is no longer active"
159
+ );
160
+ }
161
+ const changed = await applyResult(tx);
162
+ await tx.flush();
163
+ return changed;
164
+ });
165
+ Object.assign(claim.record, { status: "succeeded", result, leaseExpiresAt: null });
166
+ return statusChanged;
167
+ }
168
+ async function failPaymentOperation(em, claim) {
169
+ const failed = await em.nativeUpdate(
170
+ GatewayPaymentOperation,
171
+ { id: claim.record.id, status: "in_progress", attemptToken: claim.attemptToken },
172
+ { status: "failed", leaseExpiresAt: null, updatedAt: /* @__PURE__ */ new Date() }
173
+ );
174
+ if (failed === 1) {
175
+ Object.assign(claim.record, { status: "failed", leaseExpiresAt: null });
176
+ }
177
+ }
178
+ export {
179
+ completePaymentOperation,
180
+ failPaymentOperation,
181
+ preparePaymentOperation
182
+ };
183
+ //# sourceMappingURL=payment-operation-idempotency.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/payment_gateways/lib/payment-operation-idempotency.ts"],
4
+ "sourcesContent": ["import { createHash, randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { GatewayPaymentOperation } from '../data/entities'\nimport type { ManualGatewayAction } from './status-machine'\n\ntype Scope = { organizationId: string; tenantId: string }\ntype OperationStatus = 'in_progress' | 'succeeded' | 'failed'\n\nconst OPERATION_LEASE_MS = 10 * 60 * 1000\nconst OPERATION_UNIQUE_CONSTRAINT = 'gateway_payment_operations_scope_operation_unique'\n\nexport type ClaimedPaymentOperation = {\n record: GatewayPaymentOperation\n attemptToken: string\n providerIdempotencyKey: string\n}\n\nexport type PreparedPaymentOperation =\n | { kind: 'completed'; result: Record<string, unknown> }\n | { kind: 'claimed'; claim: ClaimedPaymentOperation }\n\ntype PreparePaymentOperationInput = {\n em: EntityManager\n transactionId: string\n providerKey: string\n action: ManualGatewayAction\n operationId?: string\n payload: Record<string, unknown>\n scope: Scope\n assertInitialAllowed: () => void\n}\n\nfunction digest(value: unknown): string {\n return createHash('sha256').update(JSON.stringify(value)).digest('hex')\n}\n\nfunction operationConflict(code: string, operationId: string, message: string): CrudHttpError {\n return new CrudHttpError(409, { error: message, code, operationId })\n}\n\nasync function findOperation(\n em: EntityManager,\n operationId: string,\n scope: Scope,\n): Promise<GatewayPaymentOperation | null> {\n return findOneWithDecryption(\n em,\n GatewayPaymentOperation,\n {\n operationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n}\n\nfunction buildOperationIdentity(input: Omit<PreparePaymentOperationInput, 'em' | 'assertInitialAllowed'>) {\n const requestHash = digest({\n transactionId: input.transactionId,\n providerKey: input.providerKey,\n action: input.action,\n payload: input.payload,\n })\n const operationId = input.operationId ?? `auto_${requestHash}`\n const providerIdempotencyKey = `om-pg-${digest({\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n transactionId: input.transactionId,\n action: input.action,\n operationId,\n })}`\n return { operationId, providerIdempotencyKey, requestHash }\n}\n\nfunction claimFields(now: Date) {\n return {\n status: 'in_progress' as OperationStatus,\n attemptToken: randomUUID(),\n leaseExpiresAt: new Date(now.getTime() + OPERATION_LEASE_MS),\n updatedAt: now,\n }\n}\n\nasync function resolveExistingOperation(\n em: EntityManager,\n existing: GatewayPaymentOperation,\n identity: ReturnType<typeof buildOperationIdentity>,\n): Promise<PreparedPaymentOperation> {\n if (existing.requestHash !== identity.requestHash) {\n throw operationConflict(\n 'payment_operation_conflict',\n identity.operationId,\n 'Payment operation id was already used with a different request',\n )\n }\n if (existing.providerIdempotencyKey !== identity.providerIdempotencyKey) {\n throw operationConflict(\n 'payment_operation_conflict',\n identity.operationId,\n 'Payment operation id resolved to a different provider request',\n )\n }\n if (existing.status === 'succeeded' && existing.result) {\n return { kind: 'completed', result: existing.result }\n }\n\n const now = new Date()\n const stale = existing.status === 'in_progress'\n && existing.leaseExpiresAt instanceof Date\n && existing.leaseExpiresAt <= now\n if (existing.status !== 'failed' && !stale) {\n throw operationConflict(\n 'payment_operation_in_progress',\n identity.operationId,\n 'Payment operation is already in progress',\n )\n }\n\n const next = claimFields(now)\n const where = existing.status === 'failed'\n ? { id: existing.id, status: 'failed', attemptToken: existing.attemptToken }\n : { id: existing.id, status: 'in_progress', attemptToken: existing.attemptToken, leaseExpiresAt: { $lt: now } }\n const claimed = await em.nativeUpdate(\n GatewayPaymentOperation,\n where,\n { ...next, attemptCount: existing.attemptCount + 1 },\n )\n if (claimed !== 1) {\n throw operationConflict(\n 'payment_operation_in_progress',\n identity.operationId,\n 'Payment operation is already in progress',\n )\n }\n Object.assign(existing, next, { attemptCount: existing.attemptCount + 1 })\n return {\n kind: 'claimed',\n claim: {\n record: existing,\n attemptToken: next.attemptToken,\n providerIdempotencyKey: existing.providerIdempotencyKey,\n },\n }\n}\n\nexport async function preparePaymentOperation(\n input: PreparePaymentOperationInput,\n): Promise<PreparedPaymentOperation> {\n const identity = buildOperationIdentity(input)\n const existing = await findOperation(input.em, identity.operationId, input.scope)\n if (existing) {\n return resolveExistingOperation(input.em, existing, identity)\n }\n\n input.assertInitialAllowed()\n const now = new Date()\n const claim = claimFields(now)\n const record = input.em.create(GatewayPaymentOperation, {\n operationId: identity.operationId,\n transactionId: input.transactionId,\n operationType: input.action,\n providerKey: input.providerKey,\n requestHash: identity.requestHash,\n providerIdempotencyKey: identity.providerIdempotencyKey,\n status: claim.status,\n attemptToken: claim.attemptToken,\n attemptCount: 1,\n result: null,\n leaseExpiresAt: claim.leaseExpiresAt,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n createdAt: now,\n updatedAt: now,\n })\n try {\n await input.em.persist(record).flush()\n return {\n kind: 'claimed',\n claim: {\n record,\n attemptToken: claim.attemptToken,\n providerIdempotencyKey: identity.providerIdempotencyKey,\n },\n }\n } catch (error: unknown) {\n if (!isUniqueViolation(error, OPERATION_UNIQUE_CONSTRAINT) && !isUniqueViolation(error)) {\n throw error\n }\n const winner = await findOperation(input.em, identity.operationId, input.scope)\n if (!winner) throw error\n return resolveExistingOperation(input.em, winner, identity)\n }\n}\n\nexport async function completePaymentOperation<T extends Record<string, unknown>>(\n em: EntityManager,\n claim: ClaimedPaymentOperation,\n result: T,\n applyResult: (tx: EntityManager) => Promise<boolean>,\n): Promise<boolean> {\n const statusChanged = await em.transactional(async (tx) => {\n const completed = await tx.nativeUpdate(\n GatewayPaymentOperation,\n { id: claim.record.id, status: 'in_progress', attemptToken: claim.attemptToken },\n { status: 'succeeded', result, leaseExpiresAt: null, updatedAt: new Date() },\n )\n if (completed !== 1) {\n throw operationConflict(\n 'payment_operation_claim_lost',\n claim.record.operationId,\n 'Payment operation claim is no longer active',\n )\n }\n const changed = await applyResult(tx)\n await tx.flush()\n return changed\n })\n Object.assign(claim.record, { status: 'succeeded', result, leaseExpiresAt: null })\n return statusChanged\n}\n\nexport async function failPaymentOperation(\n em: EntityManager,\n claim: ClaimedPaymentOperation,\n): Promise<void> {\n const failed = await em.nativeUpdate(\n GatewayPaymentOperation,\n { id: claim.record.id, status: 'in_progress', attemptToken: claim.attemptToken },\n { status: 'failed', leaseExpiresAt: null, updatedAt: new Date() },\n )\n if (failed === 1) {\n Object.assign(claim.record, { status: 'failed', leaseExpiresAt: null })\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260709220735_payment_gateways extends Migration {
3
+ up() {
4
+ this.addSql(`create table "gateway_payment_operations" ("id" uuid not null default gen_random_uuid(), "operation_id" text not null, "transaction_id" uuid not null, "operation_type" text not null, "provider_key" text not null, "request_hash" text not null, "provider_idempotency_key" text not null, "status" text not null default 'in_progress', "attempt_token" text not null, "attempt_count" int not null default 1, "result" jsonb null, "lease_expires_at" timestamptz null, "organization_id" uuid not null, "tenant_id" uuid not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, primary key ("id"));`);
5
+ this.addSql(`create index "gateway_payment_operations_status_lease_expires_at_index" on "gateway_payment_operations" ("status", "lease_expires_at");`);
6
+ this.addSql(`create index "gateway_payment_operations_transaction_id_operatio_615c8_index" on "gateway_payment_operations" ("transaction_id", "operation_type", "organization_id", "tenant_id");`);
7
+ this.addSql(`alter table "gateway_payment_operations" add constraint "gateway_payment_operations_scope_operation_unique" unique ("operation_id", "organization_id", "tenant_id");`);
8
+ }
9
+ }
10
+ export {
11
+ Migration20260709220735_payment_gateways
12
+ };
13
+ //# sourceMappingURL=Migration20260709220735_payment_gateways.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/payment_gateways/migrations/Migration20260709220735_payment_gateways.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260709220735_payment_gateways extends Migration {\n\n override up(): void | Promise<void> {\n this.addSql(`create table \"gateway_payment_operations\" (\"id\" uuid not null default gen_random_uuid(), \"operation_id\" text not null, \"transaction_id\" uuid not null, \"operation_type\" text not null, \"provider_key\" text not null, \"request_hash\" text not null, \"provider_idempotency_key\" text not null, \"status\" text not null default 'in_progress', \"attempt_token\" text not null, \"attempt_count\" int not null default 1, \"result\" jsonb null, \"lease_expires_at\" timestamptz null, \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, primary key (\"id\"));`);\n this.addSql(`create index \"gateway_payment_operations_status_lease_expires_at_index\" on \"gateway_payment_operations\" (\"status\", \"lease_expires_at\");`);\n this.addSql(`create index \"gateway_payment_operations_transaction_id_operatio_615c8_index\" on \"gateway_payment_operations\" (\"transaction_id\", \"operation_type\", \"organization_id\", \"tenant_id\");`);\n this.addSql(`alter table \"gateway_payment_operations\" add constraint \"gateway_payment_operations_scope_operation_unique\" unique (\"operation_id\", \"organization_id\", \"tenant_id\");`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,iDAAiD,UAAU;AAAA,EAE7D,KAA2B;AAClC,SAAK,OAAO,omBAAomB;AAChnB,SAAK,OAAO,yIAAyI;AACrJ,SAAK,OAAO,qLAAqL;AACjM,SAAK,OAAO,sKAAsK;AAAA,EACpL;AAEF;",
6
+ "names": []
7
+ }
@@ -67,7 +67,7 @@ async function POST(req) {
67
67
  { lockMode: LockMode.PESSIMISTIC_WRITE },
68
68
  tenantScope
69
69
  );
70
- const quote2 = await findQuoteByToken(hashedToken) ?? await findQuoteByToken(token);
70
+ const quote2 = await findQuoteByToken(hashedToken);
71
71
  if (!quote2) {
72
72
  throw notFound(translate("sales.quotes.accept.notFound", "Quote not found."));
73
73
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/sales/api/quotes/accept/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode } from '@mikro-orm/core'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getCachedRateLimiterService } from '@open-mercato/core/bootstrap'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkRateLimit, getClientIp, rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { validateSameOriginMutationRequest } from './originGuard'\nimport { hashAuthToken } from '../../../../auth/lib/tokenHash'\nimport { SalesOrder, SalesQuote } from '../../../data/entities'\nimport { quoteAcceptSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteAcceptedAdminEmail } from '../../../emails/QuoteAcceptedAdminEmail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype ConvertToOrderResult = {\n result?: { orderId?: string } | null\n orderId?: string\n}\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst quoteAcceptRateLimitConfig = readEndpointRateLimitConfig('SALES_QUOTES_ACCEPT', {\n points: 10,\n duration: 60,\n blockDuration: 300,\n keyPrefix: 'sales_quote_accept',\n})\n\nexport async function POST(req: Request) {\n try {\n const { translate } = await resolveTranslations()\n const sameOriginViolation = validateSameOriginMutationRequest(req)\n if (sameOriginViolation) {\n return NextResponse.json(\n { error: translate('sales.quotes.accept.forbidden', 'Cross-site quote acceptance is not allowed.') },\n { status: 403 },\n )\n }\n\n const rateLimiterService = getCachedRateLimiterService()\n const clientIp = rateLimiterService ? getClientIp(req, rateLimiterService.trustProxyDepth) : null\n if (rateLimiterService && clientIp) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n quoteAcceptRateLimitConfig,\n clientIp,\n translate('api.errors.rateLimit', 'Too many requests. Please try again later.'),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n\n const { token } = quoteAcceptSchema.parse(await req.json().catch(() => ({})))\n const auth = await getAuthFromRequest(req)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const hashedToken = hashAuthToken(token)\n const tenantScope = auth?.tenantId ? { tenantId: auth.tenantId } : undefined\n\n const commandBus = container.resolve('commandBus') as CommandBus\n\n // Lock the quote, flip it to confirmed, and convert it to an order inside a\n // single transaction. The conversion command reuses this transaction (and its\n // PESSIMISTIC_WRITE lock) via ctx.transactionalEm, so the status flip and the\n // order creation are atomic: if conversion fails the whole transaction rolls\n // back, leaving the quote in its prior 'sent' state with no partial order and\n // no need for an out-of-band compensating write.\n const { quote, orderId } = await em.transactional(async (trx) => {\n const findQuoteByToken = (acceptanceToken: string) =>\n findOneWithDecryption(\n trx,\n SalesQuote,\n {\n acceptanceToken,\n ...(auth?.tenantId ? { tenantId: auth.tenantId } : {}),\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n tenantScope,\n )\n const quote = (await findQuoteByToken(hashedToken)) ?? (await findQuoteByToken(token))\n if (!quote) {\n throw notFound(translate('sales.quotes.accept.notFound', 'Quote not found.'))\n }\n\n const now = new Date()\n if (quote.validUntil && quote.validUntil.getTime() < now.getTime()) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.accept.expired', 'This quote has expired.') })\n }\n\n if ((quote.status ?? null) !== 'sent') {\n throw new CrudHttpError(400, {\n error: translate('sales.quotes.accept.invalidStatus', 'This quote cannot be accepted in its current status.'),\n })\n }\n\n quote.status = 'confirmed'\n quote.statusEntryId = await resolveStatusEntryIdByValue(trx, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'confirmed',\n })\n quote.updatedAt = now\n trx.persist(quote)\n await trx.flush()\n\n const ctx: CommandRuntimeContext = {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: quote.organizationId,\n organizationIds: [quote.organizationId],\n request: req,\n transactionalEm: trx,\n }\n\n const result = (await commandBus.execute('sales.quotes.convert_to_order', { input: { quoteId: quote.id }, ctx })) as ConvertToOrderResult | null\n const orderId = result?.result?.orderId ?? result?.orderId ?? quote.id\n\n return { quote, orderId }\n })\n\n const order = await findOneWithDecryption(em, SalesOrder, { id: orderId, deletedAt: null }, {}, tenantScope)\n const orderNumber = order?.orderNumber ?? orderId\n\n // Admin notification should not block acceptance.\n const adminEmail = process.env.ADMIN_EMAIL || ''\n if (adminEmail) {\n try {\n const appUrl = process.env.APP_URL || ''\n const orderUrl = appUrl ? `${appUrl.replace(/\\/$/, '')}/backend/sales/orders/${orderId}` : `/backend/sales/orders/${orderId}`\n\n const copy = {\n preview: translate('sales.quotes.accept.adminEmail.preview', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.accept.adminEmail.heading', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n body: translate('sales.quotes.accept.adminEmail.body', 'The customer accepted quote {quoteNumber}. An order has been created: {orderNumber}.', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n cta: translate('sales.quotes.accept.adminEmail.cta', 'View order'),\n footer: translate('sales.quotes.accept.adminEmail.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: adminEmail,\n subject: translate('sales.quotes.accept.adminSubject', 'Quote {quoteNumber} accepted \u2192 Order {orderNumber}', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n react: QuoteAcceptedAdminEmail({ orderUrl, copy }),\n })\n } catch (err) {\n logger.error('sales.quotes.accept.adminEmail failed', { err })\n }\n }\n\n return NextResponse.json({ orderId, orderNumber })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.accept failed', { err })\n return NextResponse.json({ error: translate('sales.quotes.accept.failed', 'Failed to accept quote.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Accept a quote (public)',\n methods: {\n POST: {\n summary: 'Accept quote and convert to order',\n requestBody: {\n contentType: 'application/json',\n schema: quoteAcceptSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Quote accepted and order created',\n schema: z.object({ orderId: z.string().uuid(), orderNumber: z.string() }),\n },\n { status: 400, description: 'Invalid or expired quote', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Cross-site request rejected', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Quote not found', schema: z.object({ error: z.string() }) },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode } from '@mikro-orm/core'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { getCachedRateLimiterService } from '@open-mercato/core/bootstrap'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { checkRateLimit, getClientIp, rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { validateSameOriginMutationRequest } from './originGuard'\nimport { hashAuthToken } from '../../../../auth/lib/tokenHash'\nimport { SalesOrder, SalesQuote } from '../../../data/entities'\nimport { quoteAcceptSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteAcceptedAdminEmail } from '../../../emails/QuoteAcceptedAdminEmail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype ConvertToOrderResult = {\n result?: { orderId?: string } | null\n orderId?: string\n}\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nconst quoteAcceptRateLimitConfig = readEndpointRateLimitConfig('SALES_QUOTES_ACCEPT', {\n points: 10,\n duration: 60,\n blockDuration: 300,\n keyPrefix: 'sales_quote_accept',\n})\n\nexport async function POST(req: Request) {\n try {\n const { translate } = await resolveTranslations()\n const sameOriginViolation = validateSameOriginMutationRequest(req)\n if (sameOriginViolation) {\n return NextResponse.json(\n { error: translate('sales.quotes.accept.forbidden', 'Cross-site quote acceptance is not allowed.') },\n { status: 403 },\n )\n }\n\n const rateLimiterService = getCachedRateLimiterService()\n const clientIp = rateLimiterService ? getClientIp(req, rateLimiterService.trustProxyDepth) : null\n if (rateLimiterService && clientIp) {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n quoteAcceptRateLimitConfig,\n clientIp,\n translate('api.errors.rateLimit', 'Too many requests. Please try again later.'),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n\n const { token } = quoteAcceptSchema.parse(await req.json().catch(() => ({})))\n const auth = await getAuthFromRequest(req)\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const hashedToken = hashAuthToken(token)\n const tenantScope = auth?.tenantId ? { tenantId: auth.tenantId } : undefined\n\n const commandBus = container.resolve('commandBus') as CommandBus\n\n // Lock the quote, flip it to confirmed, and convert it to an order inside a\n // single transaction. The conversion command reuses this transaction (and its\n // PESSIMISTIC_WRITE lock) via ctx.transactionalEm, so the status flip and the\n // order creation are atomic: if conversion fails the whole transaction rolls\n // back, leaving the quote in its prior 'sent' state with no partial order and\n // no need for an out-of-band compensating write.\n const { quote, orderId } = await em.transactional(async (trx) => {\n const findQuoteByToken = (acceptanceToken: string) =>\n findOneWithDecryption(\n trx,\n SalesQuote,\n {\n acceptanceToken,\n ...(auth?.tenantId ? { tenantId: auth.tenantId } : {}),\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n tenantScope,\n )\n const quote = await findQuoteByToken(hashedToken)\n if (!quote) {\n throw notFound(translate('sales.quotes.accept.notFound', 'Quote not found.'))\n }\n\n const now = new Date()\n if (quote.validUntil && quote.validUntil.getTime() < now.getTime()) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.accept.expired', 'This quote has expired.') })\n }\n\n if ((quote.status ?? null) !== 'sent') {\n throw new CrudHttpError(400, {\n error: translate('sales.quotes.accept.invalidStatus', 'This quote cannot be accepted in its current status.'),\n })\n }\n\n quote.status = 'confirmed'\n quote.statusEntryId = await resolveStatusEntryIdByValue(trx, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'confirmed',\n })\n quote.updatedAt = now\n trx.persist(quote)\n await trx.flush()\n\n const ctx: CommandRuntimeContext = {\n container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: quote.organizationId,\n organizationIds: [quote.organizationId],\n request: req,\n transactionalEm: trx,\n }\n\n const result = (await commandBus.execute('sales.quotes.convert_to_order', { input: { quoteId: quote.id }, ctx })) as ConvertToOrderResult | null\n const orderId = result?.result?.orderId ?? result?.orderId ?? quote.id\n\n return { quote, orderId }\n })\n\n const order = await findOneWithDecryption(em, SalesOrder, { id: orderId, deletedAt: null }, {}, tenantScope)\n const orderNumber = order?.orderNumber ?? orderId\n\n // Admin notification should not block acceptance.\n const adminEmail = process.env.ADMIN_EMAIL || ''\n if (adminEmail) {\n try {\n const appUrl = process.env.APP_URL || ''\n const orderUrl = appUrl ? `${appUrl.replace(/\\/$/, '')}/backend/sales/orders/${orderId}` : `/backend/sales/orders/${orderId}`\n\n const copy = {\n preview: translate('sales.quotes.accept.adminEmail.preview', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.accept.adminEmail.heading', 'Quote {quoteNumber} accepted', { quoteNumber: quote.quoteNumber }),\n body: translate('sales.quotes.accept.adminEmail.body', 'The customer accepted quote {quoteNumber}. An order has been created: {orderNumber}.', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n cta: translate('sales.quotes.accept.adminEmail.cta', 'View order'),\n footer: translate('sales.quotes.accept.adminEmail.footer', 'Open Mercato'),\n }\n\n await sendEmail({\n to: adminEmail,\n subject: translate('sales.quotes.accept.adminSubject', 'Quote {quoteNumber} accepted \u2192 Order {orderNumber}', {\n quoteNumber: quote.quoteNumber,\n orderNumber,\n }),\n react: QuoteAcceptedAdminEmail({ orderUrl, copy }),\n })\n } catch (err) {\n logger.error('sales.quotes.accept.adminEmail failed', { err })\n }\n }\n\n return NextResponse.json({ orderId, orderNumber })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.accept failed', { err })\n return NextResponse.json({ error: translate('sales.quotes.accept.failed', 'Failed to accept quote.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Accept a quote (public)',\n methods: {\n POST: {\n summary: 'Accept quote and convert to order',\n requestBody: {\n contentType: 'application/json',\n schema: quoteAcceptSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Quote accepted and order created',\n schema: z.object({ orderId: z.string().uuid(), orderNumber: z.string() }),\n },\n { status: 400, description: 'Invalid or expired quote', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Cross-site request rejected', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Quote not found', schema: z.object({ error: z.string() }) },\n { status: 429, description: 'Too many requests', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["quote", "orderId"]
7
7
  }
@@ -29,9 +29,6 @@ async function GET(req, ctx) {
29
29
  const quote = await findOneWithDecryption(em, SalesQuote, {
30
30
  acceptanceToken: hashedToken,
31
31
  deletedAt: null
32
- }) ?? await findOneWithDecryption(em, SalesQuote, {
33
- acceptanceToken: token,
34
- deletedAt: null
35
32
  });
36
33
  const { translate } = await resolveTranslations();
37
34
  if (!quote) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/sales/api/quotes/public/%5Btoken%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport { resolveTranslations } from \"@open-mercato/shared/lib/i18n/server\";\nimport { isCrudHttpError, notFound } from \"@open-mercato/shared/lib/crud/errors\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { findOneWithDecryption, findWithDecryption } from \"@open-mercato/shared/lib/encryption/find\";\nimport { hashAuthToken } from \"../../../../../auth/lib/tokenHash\";\nimport {\n SalesQuote,\n SalesQuoteLine,\n SalesQuoteAdjustment,\n} from \"../../../../data/entities\";\nimport { canonicalizeUnitCode } from \"@open-mercato/shared/lib/units/unitCodes\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst paramsSchema = z.object({\n token: z.string().uuid(),\n});\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(req: Request, ctx: { params: { token: string } }) {\n try {\n const { token } = paramsSchema.parse(ctx.params ?? {});\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const hashedToken = hashAuthToken(token);\n const quote =\n (await findOneWithDecryption(em, SalesQuote, {\n acceptanceToken: hashedToken,\n deletedAt: null,\n })) ??\n (await findOneWithDecryption(em, SalesQuote, {\n acceptanceToken: token,\n deletedAt: null,\n }));\n const { translate } = await resolveTranslations();\n if (!quote) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const auth = await getAuthFromRequest(req);\n if (auth?.tenantId && quote.tenantId !== auth.tenantId) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const now = new Date();\n const isExpired =\n !!quote.validUntil && quote.validUntil.getTime() < now.getTime();\n\n const [lines, adjustments] = await Promise.all([\n findWithDecryption(\n em,\n SalesQuoteLine,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId, deletedAt: null },\n { orderBy: { lineNumber: \"asc\" } },\n ),\n findWithDecryption(\n em,\n SalesQuoteAdjustment,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },\n { orderBy: { position: \"asc\" } },\n ),\n ]);\n\n return NextResponse.json({\n quote: {\n quoteNumber: quote.quoteNumber,\n currencyCode: quote.currencyCode,\n validFrom: quote.validFrom?.toISOString() ?? null,\n validUntil: quote.validUntil?.toISOString() ?? null,\n status: quote.status ?? null,\n subtotalNetAmount: quote.subtotalNetAmount,\n subtotalGrossAmount: quote.subtotalGrossAmount,\n discountTotalAmount: quote.discountTotalAmount,\n taxTotalAmount: quote.taxTotalAmount,\n grandTotalNetAmount: quote.grandTotalNetAmount,\n grandTotalGrossAmount: quote.grandTotalGrossAmount,\n },\n lines: lines.map((line) => ({\n lineNumber: line.lineNumber ?? null,\n kind: line.kind,\n name: line.name ?? null,\n description: line.description ?? null,\n quantity: line.quantity,\n quantityUnit: canonicalizeUnitCode(line.quantityUnit) ?? null,\n normalizedQuantity: line.normalizedQuantity ?? line.quantity,\n normalizedUnit:\n canonicalizeUnitCode(line.normalizedUnit ?? line.quantityUnit) ??\n null,\n uomSnapshot: line.uomSnapshot\n ? {\n baseUnitCode: line.uomSnapshot.baseUnitCode ?? null,\n enteredUnitCode: line.uomSnapshot.enteredUnitCode ?? null,\n }\n : null,\n currencyCode: line.currencyCode,\n unitPriceNet: line.unitPriceNet,\n unitPriceGross: line.unitPriceGross,\n discountAmount: line.discountAmount,\n discountPercent: line.discountPercent,\n taxRate: line.taxRate,\n taxAmount: line.taxAmount,\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalGrossAmount,\n unitPriceReference: (() => {\n if (!line.uomSnapshot) return null;\n const ref = line.uomSnapshot.unitPriceReference;\n if (!ref) return null;\n return {\n enabled: ref.enabled ?? null,\n referenceUnitCode: ref.referenceUnitCode ?? null,\n baseQuantity: ref.baseQuantity ?? null,\n grossPerReference: ref.grossPerReference ?? null,\n netPerReference: ref.netPerReference ?? null,\n };\n })(),\n })),\n adjustments: adjustments.map((adj) => ({\n scope: adj.scope,\n kind: adj.kind,\n label: adj.label ?? adj.code ?? null,\n rate: adj.rate,\n amountNet: adj.amountNet,\n amountGross: adj.amountGross,\n currencyCode: adj.currencyCode ?? null,\n position: adj.position ?? null,\n quoteLineId: adj.quoteLine?.id ?? null,\n })),\n isExpired,\n });\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status });\n }\n const { translate } = await resolveTranslations();\n logger.error('sales.quotes.public failed', { err });\n return NextResponse.json(\n {\n error: translate(\"sales.quotes.public.failed\", \"Failed to load quote.\"),\n },\n { status: 400 },\n );\n }\n}\n\nconst publicQuoteResponseSchema = z.object({\n quote: z.object({\n quoteNumber: z.string(),\n currencyCode: z.string(),\n validFrom: z.string().nullable(),\n validUntil: z.string().nullable(),\n status: z.string().nullable(),\n subtotalNetAmount: z.string(),\n subtotalGrossAmount: z.string(),\n discountTotalAmount: z.string(),\n taxTotalAmount: z.string(),\n grandTotalNetAmount: z.string(),\n grandTotalGrossAmount: z.string(),\n }),\n lines: z.array(\n z.object({\n lineNumber: z.number().nullable(),\n kind: z.string(),\n name: z.string().nullable(),\n description: z.string().nullable(),\n quantity: z.string(),\n quantityUnit: z.string().nullable(),\n normalizedQuantity: z.string(),\n normalizedUnit: z.string().nullable(),\n uomSnapshot: z\n .object({\n baseUnitCode: z.string().nullable(),\n enteredUnitCode: z.string().nullable(),\n })\n .nullable()\n .optional(),\n currencyCode: z.string(),\n unitPriceNet: z.string(),\n unitPriceGross: z.string(),\n discountAmount: z.string(),\n discountPercent: z.string(),\n taxRate: z.string(),\n taxAmount: z.string(),\n totalNetAmount: z.string(),\n totalGrossAmount: z.string(),\n unitPriceReference: z\n .object({\n enabled: z.boolean().nullable().optional(),\n referenceUnitCode: z.string().nullable().optional(),\n baseQuantity: z.string().nullable().optional(),\n grossPerReference: z.string().nullable().optional(),\n netPerReference: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n }),\n ),\n adjustments: z.array(\n z.object({\n scope: z.string().nullable(),\n kind: z.string().nullable(),\n label: z.string().nullable(),\n rate: z.string().nullable(),\n amountNet: z.string().nullable(),\n amountGross: z.string().nullable(),\n currencyCode: z.string().nullable(),\n position: z.number().nullable(),\n quoteLineId: z.string().uuid().nullable(),\n }),\n ),\n isExpired: z.boolean(),\n});\n\nexport const openApi: OpenApiRouteDoc = {\n tag: \"Sales\",\n summary: \"View a quote (public)\",\n pathParams: z.object({ token: z.string().uuid() }),\n methods: {\n GET: {\n summary: \"Get quote details by acceptance token\",\n responses: [\n {\n status: 200,\n description: \"Quote details\",\n schema: publicQuoteResponseSchema,\n },\n {\n status: 404,\n description: \"Quote not found\",\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from \"next/server\";\nimport { z } from \"zod\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport { resolveTranslations } from \"@open-mercato/shared/lib/i18n/server\";\nimport { isCrudHttpError, notFound } from \"@open-mercato/shared/lib/crud/errors\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { findOneWithDecryption, findWithDecryption } from \"@open-mercato/shared/lib/encryption/find\";\nimport { hashAuthToken } from \"../../../../../auth/lib/tokenHash\";\nimport {\n SalesQuote,\n SalesQuoteLine,\n SalesQuoteAdjustment,\n} from \"../../../../data/entities\";\nimport { canonicalizeUnitCode } from \"@open-mercato/shared/lib/units/unitCodes\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst paramsSchema = z.object({\n token: z.string().uuid(),\n});\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(req: Request, ctx: { params: { token: string } }) {\n try {\n const { token } = paramsSchema.parse(ctx.params ?? {});\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const hashedToken = hashAuthToken(token);\n const quote = await findOneWithDecryption(em, SalesQuote, {\n acceptanceToken: hashedToken,\n deletedAt: null,\n });\n const { translate } = await resolveTranslations();\n if (!quote) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const auth = await getAuthFromRequest(req);\n if (auth?.tenantId && quote.tenantId !== auth.tenantId) {\n throw notFound(translate(\"sales.quotes.public.notFound\", \"Quote not found.\"));\n }\n\n const now = new Date();\n const isExpired =\n !!quote.validUntil && quote.validUntil.getTime() < now.getTime();\n\n const [lines, adjustments] = await Promise.all([\n findWithDecryption(\n em,\n SalesQuoteLine,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId, deletedAt: null },\n { orderBy: { lineNumber: \"asc\" } },\n ),\n findWithDecryption(\n em,\n SalesQuoteAdjustment,\n { quote: quote.id, organizationId: quote.organizationId, tenantId: quote.tenantId },\n { orderBy: { position: \"asc\" } },\n ),\n ]);\n\n return NextResponse.json({\n quote: {\n quoteNumber: quote.quoteNumber,\n currencyCode: quote.currencyCode,\n validFrom: quote.validFrom?.toISOString() ?? null,\n validUntil: quote.validUntil?.toISOString() ?? null,\n status: quote.status ?? null,\n subtotalNetAmount: quote.subtotalNetAmount,\n subtotalGrossAmount: quote.subtotalGrossAmount,\n discountTotalAmount: quote.discountTotalAmount,\n taxTotalAmount: quote.taxTotalAmount,\n grandTotalNetAmount: quote.grandTotalNetAmount,\n grandTotalGrossAmount: quote.grandTotalGrossAmount,\n },\n lines: lines.map((line) => ({\n lineNumber: line.lineNumber ?? null,\n kind: line.kind,\n name: line.name ?? null,\n description: line.description ?? null,\n quantity: line.quantity,\n quantityUnit: canonicalizeUnitCode(line.quantityUnit) ?? null,\n normalizedQuantity: line.normalizedQuantity ?? line.quantity,\n normalizedUnit:\n canonicalizeUnitCode(line.normalizedUnit ?? line.quantityUnit) ??\n null,\n uomSnapshot: line.uomSnapshot\n ? {\n baseUnitCode: line.uomSnapshot.baseUnitCode ?? null,\n enteredUnitCode: line.uomSnapshot.enteredUnitCode ?? null,\n }\n : null,\n currencyCode: line.currencyCode,\n unitPriceNet: line.unitPriceNet,\n unitPriceGross: line.unitPriceGross,\n discountAmount: line.discountAmount,\n discountPercent: line.discountPercent,\n taxRate: line.taxRate,\n taxAmount: line.taxAmount,\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalGrossAmount,\n unitPriceReference: (() => {\n if (!line.uomSnapshot) return null;\n const ref = line.uomSnapshot.unitPriceReference;\n if (!ref) return null;\n return {\n enabled: ref.enabled ?? null,\n referenceUnitCode: ref.referenceUnitCode ?? null,\n baseQuantity: ref.baseQuantity ?? null,\n grossPerReference: ref.grossPerReference ?? null,\n netPerReference: ref.netPerReference ?? null,\n };\n })(),\n })),\n adjustments: adjustments.map((adj) => ({\n scope: adj.scope,\n kind: adj.kind,\n label: adj.label ?? adj.code ?? null,\n rate: adj.rate,\n amountNet: adj.amountNet,\n amountGross: adj.amountGross,\n currencyCode: adj.currencyCode ?? null,\n position: adj.position ?? null,\n quoteLineId: adj.quoteLine?.id ?? null,\n })),\n isExpired,\n });\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status });\n }\n const { translate } = await resolveTranslations();\n logger.error('sales.quotes.public failed', { err });\n return NextResponse.json(\n {\n error: translate(\"sales.quotes.public.failed\", \"Failed to load quote.\"),\n },\n { status: 400 },\n );\n }\n}\n\nconst publicQuoteResponseSchema = z.object({\n quote: z.object({\n quoteNumber: z.string(),\n currencyCode: z.string(),\n validFrom: z.string().nullable(),\n validUntil: z.string().nullable(),\n status: z.string().nullable(),\n subtotalNetAmount: z.string(),\n subtotalGrossAmount: z.string(),\n discountTotalAmount: z.string(),\n taxTotalAmount: z.string(),\n grandTotalNetAmount: z.string(),\n grandTotalGrossAmount: z.string(),\n }),\n lines: z.array(\n z.object({\n lineNumber: z.number().nullable(),\n kind: z.string(),\n name: z.string().nullable(),\n description: z.string().nullable(),\n quantity: z.string(),\n quantityUnit: z.string().nullable(),\n normalizedQuantity: z.string(),\n normalizedUnit: z.string().nullable(),\n uomSnapshot: z\n .object({\n baseUnitCode: z.string().nullable(),\n enteredUnitCode: z.string().nullable(),\n })\n .nullable()\n .optional(),\n currencyCode: z.string(),\n unitPriceNet: z.string(),\n unitPriceGross: z.string(),\n discountAmount: z.string(),\n discountPercent: z.string(),\n taxRate: z.string(),\n taxAmount: z.string(),\n totalNetAmount: z.string(),\n totalGrossAmount: z.string(),\n unitPriceReference: z\n .object({\n enabled: z.boolean().nullable().optional(),\n referenceUnitCode: z.string().nullable().optional(),\n baseQuantity: z.string().nullable().optional(),\n grossPerReference: z.string().nullable().optional(),\n netPerReference: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n }),\n ),\n adjustments: z.array(\n z.object({\n scope: z.string().nullable(),\n kind: z.string().nullable(),\n label: z.string().nullable(),\n rate: z.string().nullable(),\n amountNet: z.string().nullable(),\n amountGross: z.string().nullable(),\n currencyCode: z.string().nullable(),\n position: z.number().nullable(),\n quoteLineId: z.string().uuid().nullable(),\n }),\n ),\n isExpired: z.boolean(),\n});\n\nexport const openApi: OpenApiRouteDoc = {\n tag: \"Sales\",\n summary: \"View a quote (public)\",\n pathParams: z.object({ token: z.string().uuid() }),\n methods: {\n GET: {\n summary: \"Get quote details by acceptance token\",\n responses: [\n {\n status: 200,\n description: \"Quote details\",\n schema: publicQuoteResponseSchema,\n },\n {\n status: 404,\n description: \"Quote not found\",\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,16 @@
1
+ export const id = "id";
2
+ export const operation_id = "operation_id";
3
+ export const transaction_id = "transaction_id";
4
+ export const operation_type = "operation_type";
5
+ export const provider_key = "provider_key";
6
+ export const request_hash = "request_hash";
7
+ export const provider_idempotency_key = "provider_idempotency_key";
8
+ export const status = "status";
9
+ export const attempt_token = "attempt_token";
10
+ export const attempt_count = "attempt_count";
11
+ export const result = "result";
12
+ export const lease_expires_at = "lease_expires_at";
13
+ export const organization_id = "organization_id";
14
+ export const tenant_id = "tenant_id";
15
+ export const created_at = "created_at";
16
+ export const updated_at = "updated_at";
@@ -267,6 +267,7 @@ export const M = {
267
267
  },
268
268
  "payment_gateways": {
269
269
  "gateway_transaction": "payment_gateways:gateway_transaction",
270
+ "gateway_payment_operation": "payment_gateways:gateway_payment_operation",
270
271
  "gateway_session_initialization": "payment_gateways:gateway_session_initialization",
271
272
  "webhook_processed_event": "payment_gateways:webhook_processed_event"
272
273
  },
@@ -1225,6 +1225,24 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
1225
1225
  "updated_at": "updated_at",
1226
1226
  "value": "value"
1227
1227
  },
1228
+ "gateway_payment_operation": {
1229
+ "id": "id",
1230
+ "operation_id": "operation_id",
1231
+ "transaction_id": "transaction_id",
1232
+ "operation_type": "operation_type",
1233
+ "provider_key": "provider_key",
1234
+ "request_hash": "request_hash",
1235
+ "provider_idempotency_key": "provider_idempotency_key",
1236
+ "status": "status",
1237
+ "attempt_token": "attempt_token",
1238
+ "attempt_count": "attempt_count",
1239
+ "result": "result",
1240
+ "lease_expires_at": "lease_expires_at",
1241
+ "organization_id": "organization_id",
1242
+ "tenant_id": "tenant_id",
1243
+ "created_at": "created_at",
1244
+ "updated_at": "updated_at"
1245
+ },
1228
1246
  "gateway_session_initialization": {
1229
1247
  "id": "id",
1230
1248
  "operation_key": "operation_key",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.7-develop.6569.1.c06dee4866",
3
+ "version": "0.6.7-develop.6573.1.28649ddec6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -253,16 +253,16 @@
253
253
  "zod": "^4.4.3"
254
254
  },
255
255
  "peerDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.7-develop.6569.1.c06dee4866",
257
- "@open-mercato/shared": "0.6.7-develop.6569.1.c06dee4866",
258
- "@open-mercato/ui": "0.6.7-develop.6569.1.c06dee4866",
256
+ "@open-mercato/ai-assistant": "0.6.7-develop.6573.1.28649ddec6",
257
+ "@open-mercato/shared": "0.6.7-develop.6573.1.28649ddec6",
258
+ "@open-mercato/ui": "0.6.7-develop.6573.1.28649ddec6",
259
259
  "react": "^19.0.0",
260
260
  "react-dom": "^19.0.0"
261
261
  },
262
262
  "devDependencies": {
263
- "@open-mercato/ai-assistant": "0.6.7-develop.6569.1.c06dee4866",
264
- "@open-mercato/shared": "0.6.7-develop.6569.1.c06dee4866",
265
- "@open-mercato/ui": "0.6.7-develop.6569.1.c06dee4866",
263
+ "@open-mercato/ai-assistant": "0.6.7-develop.6573.1.28649ddec6",
264
+ "@open-mercato/shared": "0.6.7-develop.6573.1.28649ddec6",
265
+ "@open-mercato/ui": "0.6.7-develop.6573.1.28649ddec6",
266
266
  "@testing-library/dom": "^10.4.1",
267
267
  "@testing-library/jest-dom": "^6.9.1",
268
268
  "@testing-library/react": "^16.3.1",
@@ -279,7 +279,7 @@ const createOrganizationCommand: CommandHandler<Record<string, unknown>, Organiz
279
279
  async execute(rawInput, ctx) {
280
280
  const { parsed, custom } = parseWithCustomFields(organizationCreateSchema, rawInput)
281
281
  const em = (ctx.container.resolve('em') as EntityManager)
282
- const tenantId = await enforceTenantSelection(ctx, parsed.tenantId ?? null)
282
+ const tenantId = await enforceTenantSelection(ctx, parsed.tenantId)
283
283
  if (!tenantId) throw new CrudHttpError(400, { error: 'Tenant scope required' })
284
284
 
285
285
  const parentId = parsed.parentId ?? null
@@ -61,6 +61,7 @@ export async function POST(req: Request) {
61
61
  parsed.data.transactionId,
62
62
  parsed.data.reason,
63
63
  { organizationId: auth.orgId as string, tenantId: auth.tenantId },
64
+ parsed.data.operationId,
64
65
  )
65
66
  await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
66
67
  tenantId: auth.tenantId,
@@ -61,6 +61,7 @@ export async function POST(req: Request) {
61
61
  parsed.data.transactionId,
62
62
  parsed.data.amount,
63
63
  { organizationId: auth.orgId as string, tenantId: auth.tenantId },
64
+ parsed.data.operationId,
64
65
  )
65
66
  await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
66
67
  tenantId: auth.tenantId,
@@ -62,6 +62,7 @@ export async function POST(req: Request) {
62
62
  parsed.data.amount,
63
63
  parsed.data.reason,
64
64
  { organizationId: auth.orgId as string, tenantId: auth.tenantId },
65
+ parsed.data.operationId,
65
66
  )
66
67
  await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
67
68
  tenantId: auth.tenantId,
@@ -75,6 +75,65 @@ export class GatewayTransaction {
75
75
  deletedAt?: Date | null
76
76
  }
77
77
 
78
+ @Entity({ tableName: 'gateway_payment_operations' })
79
+ @Unique({
80
+ name: 'gateway_payment_operations_scope_operation_unique',
81
+ properties: ['operationId', 'organizationId', 'tenantId'],
82
+ })
83
+ @Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })
84
+ @Index({ properties: ['status', 'leaseExpiresAt'] })
85
+ export class GatewayPaymentOperation {
86
+ [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'
87
+
88
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
89
+ id!: string
90
+
91
+ @Property({ name: 'operation_id', type: 'text' })
92
+ operationId!: string
93
+
94
+ @Property({ name: 'transaction_id', type: 'uuid' })
95
+ transactionId!: string
96
+
97
+ @Property({ name: 'operation_type', type: 'text' })
98
+ operationType!: string
99
+
100
+ @Property({ name: 'provider_key', type: 'text' })
101
+ providerKey!: string
102
+
103
+ @Property({ name: 'request_hash', type: 'text' })
104
+ requestHash!: string
105
+
106
+ @Property({ name: 'provider_idempotency_key', type: 'text' })
107
+ providerIdempotencyKey!: string
108
+
109
+ @Property({ name: 'status', type: 'text' })
110
+ status: string = 'in_progress'
111
+
112
+ @Property({ name: 'attempt_token', type: 'text' })
113
+ attemptToken!: string
114
+
115
+ @Property({ name: 'attempt_count', type: 'integer' })
116
+ attemptCount: number = 1
117
+
118
+ @Property({ name: 'result', type: 'jsonb', nullable: true })
119
+ result?: Record<string, unknown> | null
120
+
121
+ @Property({ name: 'lease_expires_at', type: Date, nullable: true })
122
+ leaseExpiresAt?: Date | null
123
+
124
+ @Property({ name: 'organization_id', type: 'uuid' })
125
+ organizationId!: string
126
+
127
+ @Property({ name: 'tenant_id', type: 'uuid' })
128
+ tenantId!: string
129
+
130
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
131
+ createdAt: Date = new Date()
132
+
133
+ @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })
134
+ updatedAt: Date = new Date()
135
+ }
136
+
78
137
  @Entity({ tableName: 'gateway_session_initializations' })
79
138
  @Unique({
80
139
  name: 'gateway_session_initializations_scope_operation_unique',
@@ -36,6 +36,7 @@ export type CreateSessionPayload = z.infer<typeof createSessionSchema>
36
36
  export const captureSchema = z.object({
37
37
  transactionId: z.string().uuid(),
38
38
  amount: z.number().positive().optional(),
39
+ operationId: z.string().trim().min(1).max(200).optional(),
39
40
  })
40
41
 
41
42
  export type CapturePayload = z.infer<typeof captureSchema>
@@ -44,6 +45,7 @@ export const refundSchema = z.object({
44
45
  transactionId: z.string().uuid(),
45
46
  amount: z.number().positive().optional(),
46
47
  reason: z.string().max(200).optional(),
48
+ operationId: z.string().trim().min(1).max(200).optional(),
47
49
  })
48
50
 
49
51
  export type RefundPayload = z.infer<typeof refundSchema>
@@ -51,6 +53,7 @@ export type RefundPayload = z.infer<typeof refundSchema>
51
53
  export const cancelSchema = z.object({
52
54
  transactionId: z.string().uuid(),
53
55
  reason: z.string().max(200).optional(),
56
+ operationId: z.string().trim().min(1).max(200).optional(),
54
57
  })
55
58
 
56
59
  export type CancelPayload = z.infer<typeof cancelSchema>