@endora-commerce/mod-quote-requests 0.100.0

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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/index.d.ts +26 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +56 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/pages/RfqCreatePage.d.ts +9 -0
  8. package/dist/admin/pages/RfqCreatePage.d.ts.map +1 -0
  9. package/dist/admin/pages/RfqCreatePage.js +163 -0
  10. package/dist/admin/pages/RfqCreatePage.js.map +1 -0
  11. package/dist/admin/pages/RfqDetail.d.ts +9 -0
  12. package/dist/admin/pages/RfqDetail.d.ts.map +1 -0
  13. package/dist/admin/pages/RfqDetail.js +399 -0
  14. package/dist/admin/pages/RfqDetail.js.map +1 -0
  15. package/dist/admin/pages/RfqList.d.ts +9 -0
  16. package/dist/admin/pages/RfqList.d.ts.map +1 -0
  17. package/dist/admin/pages/RfqList.js +214 -0
  18. package/dist/admin/pages/RfqList.js.map +1 -0
  19. package/dist/admin/validity.d.ts +52 -0
  20. package/dist/admin/validity.d.ts.map +1 -0
  21. package/dist/admin/validity.js +61 -0
  22. package/dist/admin/validity.js.map +1 -0
  23. package/dist/backend/entities/quote-request-event.entity.d.ts +24 -0
  24. package/dist/backend/entities/quote-request-event.entity.d.ts.map +1 -0
  25. package/dist/backend/entities/quote-request-event.entity.js +67 -0
  26. package/dist/backend/entities/quote-request-event.entity.js.map +1 -0
  27. package/dist/backend/entities/quote-request-item.entity.d.ts +34 -0
  28. package/dist/backend/entities/quote-request-item.entity.d.ts.map +1 -0
  29. package/dist/backend/entities/quote-request-item.entity.js +119 -0
  30. package/dist/backend/entities/quote-request-item.entity.js.map +1 -0
  31. package/dist/backend/entities/quote-request-notification-event.entity.d.ts +23 -0
  32. package/dist/backend/entities/quote-request-notification-event.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/quote-request-notification-event.entity.js +72 -0
  34. package/dist/backend/entities/quote-request-notification-event.entity.js.map +1 -0
  35. package/dist/backend/entities/quote-request-revision.entity.d.ts +35 -0
  36. package/dist/backend/entities/quote-request-revision.entity.d.ts.map +1 -0
  37. package/dist/backend/entities/quote-request-revision.entity.js +67 -0
  38. package/dist/backend/entities/quote-request-revision.entity.js.map +1 -0
  39. package/dist/backend/entities/quote-request.entity.d.ts +90 -0
  40. package/dist/backend/entities/quote-request.entity.d.ts.map +1 -0
  41. package/dist/backend/entities/quote-request.entity.js +191 -0
  42. package/dist/backend/entities/quote-request.entity.js.map +1 -0
  43. package/dist/backend/index.d.ts +120 -0
  44. package/dist/backend/index.d.ts.map +1 -0
  45. package/dist/backend/index.js +361 -0
  46. package/dist/backend/index.js.map +1 -0
  47. package/dist/backend/plugin.d.ts +95 -0
  48. package/dist/backend/plugin.d.ts.map +1 -0
  49. package/dist/backend/plugin.js +152 -0
  50. package/dist/backend/plugin.js.map +1 -0
  51. package/dist/backend/routes.admin.d.ts +13 -0
  52. package/dist/backend/routes.admin.d.ts.map +1 -0
  53. package/dist/backend/routes.admin.js +82 -0
  54. package/dist/backend/routes.admin.js.map +1 -0
  55. package/dist/backend/routes.customer.d.ts +18 -0
  56. package/dist/backend/routes.customer.d.ts.map +1 -0
  57. package/dist/backend/routes.customer.js +98 -0
  58. package/dist/backend/routes.customer.js.map +1 -0
  59. package/dist/backend/routes.sales-reps.d.ts +12 -0
  60. package/dist/backend/routes.sales-reps.d.ts.map +1 -0
  61. package/dist/backend/routes.sales-reps.js +57 -0
  62. package/dist/backend/routes.sales-reps.js.map +1 -0
  63. package/dist/backend/services/order-completion-reactor.d.ts +32 -0
  64. package/dist/backend/services/order-completion-reactor.d.ts.map +1 -0
  65. package/dist/backend/services/order-completion-reactor.js +57 -0
  66. package/dist/backend/services/order-completion-reactor.js.map +1 -0
  67. package/dist/backend/services/quote-request-business-id-generator.d.ts +46 -0
  68. package/dist/backend/services/quote-request-business-id-generator.d.ts.map +1 -0
  69. package/dist/backend/services/quote-request-business-id-generator.js +69 -0
  70. package/dist/backend/services/quote-request-business-id-generator.js.map +1 -0
  71. package/dist/backend/services/quote-request-read-port.d.ts +36 -0
  72. package/dist/backend/services/quote-request-read-port.d.ts.map +1 -0
  73. package/dist/backend/services/quote-request-read-port.js +96 -0
  74. package/dist/backend/services/quote-request-read-port.js.map +1 -0
  75. package/dist/backend/services/raised-on-channel.d.ts +23 -0
  76. package/dist/backend/services/raised-on-channel.d.ts.map +1 -0
  77. package/dist/backend/services/raised-on-channel.js +26 -0
  78. package/dist/backend/services/raised-on-channel.js.map +1 -0
  79. package/dist/backend/services/rfq-admin-service.d.ts +75 -0
  80. package/dist/backend/services/rfq-admin-service.d.ts.map +1 -0
  81. package/dist/backend/services/rfq-admin-service.js +487 -0
  82. package/dist/backend/services/rfq-admin-service.js.map +1 -0
  83. package/dist/backend/services/rfq-event-service.d.ts +32 -0
  84. package/dist/backend/services/rfq-event-service.d.ts.map +1 -0
  85. package/dist/backend/services/rfq-event-service.js +28 -0
  86. package/dist/backend/services/rfq-event-service.js.map +1 -0
  87. package/dist/backend/services/rfq-expiry-worker.d.ts +50 -0
  88. package/dist/backend/services/rfq-expiry-worker.d.ts.map +1 -0
  89. package/dist/backend/services/rfq-expiry-worker.js +70 -0
  90. package/dist/backend/services/rfq-expiry-worker.js.map +1 -0
  91. package/dist/backend/services/rfq-notification-service.d.ts +36 -0
  92. package/dist/backend/services/rfq-notification-service.d.ts.map +1 -0
  93. package/dist/backend/services/rfq-notification-service.js +52 -0
  94. package/dist/backend/services/rfq-notification-service.js.map +1 -0
  95. package/dist/backend/services/rfq-revision-service.d.ts +70 -0
  96. package/dist/backend/services/rfq-revision-service.d.ts.map +1 -0
  97. package/dist/backend/services/rfq-revision-service.js +120 -0
  98. package/dist/backend/services/rfq-revision-service.js.map +1 -0
  99. package/dist/backend/services/rfq-service.d.ts +140 -0
  100. package/dist/backend/services/rfq-service.d.ts.map +1 -0
  101. package/dist/backend/services/rfq-service.js +786 -0
  102. package/dist/backend/services/rfq-service.js.map +1 -0
  103. package/dist/backend/services/sales-channel-attributions.d.ts +38 -0
  104. package/dist/backend/services/sales-channel-attributions.d.ts.map +1 -0
  105. package/dist/backend/services/sales-channel-attributions.js +48 -0
  106. package/dist/backend/services/sales-channel-attributions.js.map +1 -0
  107. package/dist/manifest.d.ts +203 -0
  108. package/dist/manifest.d.ts.map +1 -0
  109. package/dist/manifest.js +277 -0
  110. package/dist/manifest.js.map +1 -0
  111. package/dist/migrations/20260424T190112_quote_requests_init.d.ts +19 -0
  112. package/dist/migrations/20260424T190112_quote_requests_init.d.ts.map +1 -0
  113. package/dist/migrations/20260424T190112_quote_requests_init.js +65 -0
  114. package/dist/migrations/20260424T190112_quote_requests_init.js.map +1 -0
  115. package/dist/migrations/20260503T141344_quote_requests_workflow.d.ts +37 -0
  116. package/dist/migrations/20260503T141344_quote_requests_workflow.d.ts.map +1 -0
  117. package/dist/migrations/20260503T141344_quote_requests_workflow.js +251 -0
  118. package/dist/migrations/20260503T141344_quote_requests_workflow.js.map +1 -0
  119. package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.d.ts +10 -0
  120. package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.d.ts.map +1 -0
  121. package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.js +16 -0
  122. package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.js.map +1 -0
  123. package/dist/migrations/20260611T140417_quote_requests_business_id.d.ts +24 -0
  124. package/dist/migrations/20260611T140417_quote_requests_business_id.d.ts.map +1 -0
  125. package/dist/migrations/20260611T140417_quote_requests_business_id.js +34 -0
  126. package/dist/migrations/20260611T140417_quote_requests_business_id.js.map +1 -0
  127. package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.d.ts +21 -0
  128. package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.d.ts.map +1 -0
  129. package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.js +29 -0
  130. package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.js.map +1 -0
  131. package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.d.ts +12 -0
  132. package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.d.ts.map +1 -0
  133. package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.js +16 -0
  134. package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.js.map +1 -0
  135. package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.d.ts +34 -0
  136. package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.d.ts.map +1 -0
  137. package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.js +57 -0
  138. package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.js.map +1 -0
  139. package/dist/migrations/index.d.ts +53 -0
  140. package/dist/migrations/index.d.ts.map +1 -0
  141. package/dist/migrations/index.js +61 -0
  142. package/dist/migrations/index.js.map +1 -0
  143. package/dist/migrations/status-mapping.d.ts +18 -0
  144. package/dist/migrations/status-mapping.d.ts.map +1 -0
  145. package/dist/migrations/status-mapping.js +28 -0
  146. package/dist/migrations/status-mapping.js.map +1 -0
  147. package/docs/quote_requests.md +168 -0
  148. package/i18n/en.json +14 -0
  149. package/i18n/pl.json +14 -0
  150. package/package.json +99 -0
  151. package/tailwind.css +14 -0
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { QuoteRequest } from '../entities/quote-request.entity.js';
3
+ import { withSystemScope } from '@endora-commerce/platform/tenancy';
4
+ export class RfqExpiryWorker {
5
+ deps;
6
+ constructor(deps) {
7
+ this.deps = deps;
8
+ }
9
+ async sweep(now = new Date()) {
10
+ // command-coverage-ignore: background expiry sweep — flips overdue RFQs to
11
+ // Expired on a timer, a system lifecycle job, not an operator-initiated write.
12
+ // Feature 050 — system-wide sweep across all orgs; run under a system scope
13
+ // so the QuoteRequest reads/writes carry a tenant context (fail-closed guard).
14
+ return withSystemScope('rfq-expiry sweep', async () => {
15
+ const expiryDays = await this.deps.resolveExpiryDays();
16
+ if (!Number.isFinite(expiryDays) || expiryDays <= 0) {
17
+ return { expiredCount: 0 };
18
+ }
19
+ const em = this.deps.emFactory();
20
+ const cutoff = new Date(now.getTime() - expiryDays * 86_400_000);
21
+ const expirable = await em.find(QuoteRequest, {
22
+ status: { $in: ['Pending', 'Created from admin'] },
23
+ updatedAt: { $lt: cutoff },
24
+ });
25
+ if (expirable.length === 0)
26
+ return { expiredCount: 0 };
27
+ for (const rfq of expirable) {
28
+ rfq.status = 'Expired';
29
+ rfq.expiredAt = now;
30
+ rfq.version += 1;
31
+ }
32
+ await em.flush();
33
+ for (const rfq of expirable) {
34
+ const evt = await this.deps.eventService.append({
35
+ quoteRequestId: rfq.id,
36
+ eventType: 'expired',
37
+ actor: { roleLabel: 'System' },
38
+ payload: { type: 'expired', expiryDaysAtTime: expiryDays },
39
+ });
40
+ const recipients = [
41
+ { customerAccountId: rfq.customerAccountId },
42
+ ];
43
+ const assignments = await this.deps.salesRepAssignment.listForOrganization(rfq.organizationId);
44
+ if (assignments.length > 0) {
45
+ for (const a of assignments)
46
+ recipients.push({ adminUserId: a.adminUserId });
47
+ }
48
+ else {
49
+ // Unassigned-org fallback — notify every active admin.
50
+ const everyAdmin = await this.deps.adminUsers.listAll();
51
+ for (const a of everyAdmin)
52
+ recipients.push({ adminUserId: a.id });
53
+ }
54
+ await this.deps.notificationService.enqueue({
55
+ quoteRequestId: rfq.id,
56
+ sourceEventId: evt.id,
57
+ recipients,
58
+ channels: ['email', 'in_app'],
59
+ });
60
+ this.deps.events.emit('rfq.expired.v1', {
61
+ eventId: randomUUID(),
62
+ occurredAt: now.toISOString(),
63
+ rfqId: rfq.id,
64
+ });
65
+ }
66
+ return { expiredCount: expirable.length };
67
+ });
68
+ }
69
+ }
70
+ //# sourceMappingURL=rfq-expiry-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfq-expiry-worker.js","sourceRoot":"","sources":["../../../src/backend/services/rfq-expiry-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type QuoteRequestNotificationChannel } from '../entities/quote-request-notification-event.entity.js';
3
+ /**
4
+ * Notification fan-out for Quote Request lifecycle transitions.
5
+ *
6
+ * The unique index on
7
+ * (quote_request_id, source_event_id, recipient*, channel) is enforced
8
+ * at the DB level (migration 029); duplicate-row attempts are caught
9
+ * here as Postgres `23505` and silently ignored, keeping retries
10
+ * idempotent (FR-030 + research §R4).
11
+ *
12
+ * Actual transport (email send, in-app surface push) is handled by the
13
+ * foundation `notifications` BullMQ queue — out of scope for this
14
+ * service. We only persist the row and mark it `queued`; the queue
15
+ * worker flips it to `sent` / `failed`.
16
+ */
17
+ export interface NotificationRecipient {
18
+ adminUserId?: string | null;
19
+ customerAccountId?: string | null;
20
+ }
21
+ export interface EnqueueNotificationInput {
22
+ quoteRequestId: string;
23
+ sourceEventId: string;
24
+ recipients: NotificationRecipient[];
25
+ channels: QuoteRequestNotificationChannel[];
26
+ }
27
+ export declare class RfqNotificationService {
28
+ private readonly emFactory;
29
+ constructor(emFactory: () => EntityManager);
30
+ /**
31
+ * Persist one row per recipient × channel. Returns the number of
32
+ * rows actually inserted (after dedupe).
33
+ */
34
+ enqueue(input: EnqueueNotificationInput): Promise<number>;
35
+ }
36
+ //# sourceMappingURL=rfq-notification-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfq-notification-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/rfq-notification-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,wDAAwD,CAAC;AAEhE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,qBAAqB,EAAE,CAAC;IACpC,QAAQ,EAAE,+BAA+B,EAAE,CAAC;CAC7C;AAED,qBAAa,sBAAsB;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAE3D;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC;CAgChE"}
@@ -0,0 +1,52 @@
1
+ import { QuoteRequestNotificationEvent, } from '../entities/quote-request-notification-event.entity.js';
2
+ export class RfqNotificationService {
3
+ emFactory;
4
+ constructor(emFactory) {
5
+ this.emFactory = emFactory;
6
+ }
7
+ /**
8
+ * Persist one row per recipient × channel. Returns the number of
9
+ * rows actually inserted (after dedupe).
10
+ */
11
+ async enqueue(input) {
12
+ // command-coverage-ignore: notification delivery queue — writes per-recipient
13
+ // dispatch rows, delivery bookkeeping, not an audited domain-state mutation.
14
+ const em = this.emFactory();
15
+ let inserted = 0;
16
+ for (const recipient of input.recipients) {
17
+ // Skip empty recipient (would violate the actor-XOR invariant if both
18
+ // adminUserId and customerAccountId are null).
19
+ if (!recipient.adminUserId && !recipient.customerAccountId)
20
+ continue;
21
+ for (const channel of input.channels) {
22
+ const event = em.create(QuoteRequestNotificationEvent, {
23
+ quoteRequestId: input.quoteRequestId,
24
+ sourceEventId: input.sourceEventId,
25
+ recipientAdminUserId: recipient.adminUserId ?? null,
26
+ recipientCustomerAccountId: recipient.customerAccountId ?? null,
27
+ channel,
28
+ status: 'queued',
29
+ });
30
+ try {
31
+ await em.persistAndFlush(event);
32
+ inserted += 1;
33
+ }
34
+ catch (err) {
35
+ // Postgres unique-violation on (rfq, event, recipient, channel) —
36
+ // this is the dedupe guarantee firing on a retry.
37
+ em.clear();
38
+ if (!isUniqueViolation(err))
39
+ throw err;
40
+ }
41
+ }
42
+ }
43
+ return inserted;
44
+ }
45
+ }
46
+ function isUniqueViolation(err) {
47
+ if (typeof err !== 'object' || err === null)
48
+ return false;
49
+ const candidate = err;
50
+ return candidate.code === '23505';
51
+ }
52
+ //# sourceMappingURL=rfq-notification-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfq-notification-service.js","sourceRoot":"","sources":["../../../src/backend/services/rfq-notification-service.ts"],"names":[],"mappings":"AACA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wDAAwD,CAAC;AA4BhE,MAAM,OAAO,sBAAsB;IACJ;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,KAA+B;QAC3C,8EAA8E;QAC9E,6EAA6E;QAC7E,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACzC,sEAAsE;YACtE,+CAA+C;YAC/C,IAAI,CAAC,SAAS,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,iBAAiB;gBAAE,SAAS;YAErE,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACrC,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,CAAC,6BAA6B,EAAE;oBACrD,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,aAAa,EAAE,KAAK,CAAC,aAAa;oBAClC,oBAAoB,EAAE,SAAS,CAAC,WAAW,IAAI,IAAI;oBACnD,0BAA0B,EAAE,SAAS,CAAC,iBAAiB,IAAI,IAAI;oBAC/D,OAAO;oBACP,MAAM,EAAE,QAAQ;iBACjB,CAAC,CAAC;gBACH,IAAI,CAAC;oBACH,MAAM,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;oBAChC,QAAQ,IAAI,CAAC,CAAC;gBAChB,CAAC;gBAAC,OAAO,GAAY,EAAE,CAAC;oBACtB,kEAAkE;oBAClE,kDAAkD;oBAClD,EAAE,CAAC,KAAK,EAAE,CAAC;oBACX,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC;wBAAE,MAAM,GAAG,CAAC;gBACzC,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF;AAED,SAAS,iBAAiB,CAAC,GAAY;IACrC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,SAAS,GAAG,GAAyB,CAAC;IAC5C,OAAO,SAAS,CAAC,IAAI,KAAK,OAAO,CAAC;AACpC,CAAC"}
@@ -0,0 +1,70 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { QuoteRequestRevision, type QuoteRequestRevisionLine } from '../entities/quote-request-revision.entity.js';
3
+ import type { QuoteRequest } from '../entities/quote-request.entity.js';
4
+ import type { QuoteRequestItem } from '../entities/quote-request-item.entity.js';
5
+ /**
6
+ * Coarse-grained revision snapshot writer + diff calculator. Each
7
+ * `modify` event in the lifecycle creates one row in
8
+ * quote_request_revisions; the customer-facing comparison view
9
+ * (`comparisonAgainstLastSeen`) is computed on the fly by
10
+ * `diffRevisions` rather than being persisted.
11
+ */
12
+ export type RfqDiffEntry = {
13
+ kind: 'header_note';
14
+ before: string | null;
15
+ after: string | null;
16
+ } | {
17
+ kind: 'line_added';
18
+ productId: string;
19
+ productName: string;
20
+ quantity: number;
21
+ agreedUnitPrice: number | null;
22
+ } | {
23
+ kind: 'line_removed';
24
+ productId: string;
25
+ productName: string;
26
+ } | {
27
+ kind: 'line_quantity';
28
+ productId: string;
29
+ productName: string;
30
+ before: number;
31
+ after: number;
32
+ } | {
33
+ kind: 'line_agreed_unit_price';
34
+ productId: string;
35
+ productName: string;
36
+ before: number | null;
37
+ after: number | null;
38
+ };
39
+ export interface RecordRevisionInput {
40
+ rfq: QuoteRequest;
41
+ items: QuoteRequestItem[];
42
+ actor: {
43
+ adminUserId?: string | null;
44
+ customerAccountId?: string | null;
45
+ };
46
+ previousRevisionId?: string | null;
47
+ }
48
+ export declare class RfqRevisionService {
49
+ private readonly emFactory;
50
+ constructor(emFactory: () => EntityManager);
51
+ record(input: RecordRevisionInput): Promise<QuoteRequestRevision>;
52
+ /** Returns the latest revision for an RFQ, if any. */
53
+ latestForRfq(quoteRequestId: string): Promise<QuoteRequestRevision | null>;
54
+ /** Returns the revision whose `revisionNumber === n`, if any. */
55
+ byNumber(quoteRequestId: string, n: number): Promise<QuoteRequestRevision | null>;
56
+ /**
57
+ * Pure diff calculator. Takes two snapshots (`before`, `after`) plus
58
+ * their header notes and returns the list of diff entries in the
59
+ * order header → line_added → line_removed → line_quantity →
60
+ * line_agreed_unit_price. Stable by productId so renderers can
61
+ * group/sort easily.
62
+ */
63
+ diffRevisions(args: {
64
+ beforeHeaderNote: string | null;
65
+ afterHeaderNote: string | null;
66
+ beforeItems: QuoteRequestRevisionLine[];
67
+ afterItems: QuoteRequestRevisionLine[];
68
+ }): RfqDiffEntry[];
69
+ }
70
+ //# sourceMappingURL=rfq-revision-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfq-revision-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/rfq-revision-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,oBAAoB,EACpB,KAAK,wBAAwB,EAC9B,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACpE;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,GACD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAChE;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,IAAI,EAAE,wBAAwB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEN,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,YAAY,CAAC;IAClB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC1E,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAsBvE,sDAAsD;IAChD,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAUhF,iEAAiE;IAC3D,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAUvF;;;;;;OAMG;IACH,aAAa,CAAC,IAAI,EAAE;QAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,WAAW,EAAE,wBAAwB,EAAE,CAAC;QACxC,UAAU,EAAE,wBAAwB,EAAE,CAAC;KACxC,GAAG,YAAY,EAAE;CA+DnB"}
@@ -0,0 +1,120 @@
1
+ import { QuoteRequestRevision, } from '../entities/quote-request-revision.entity.js';
2
+ export class RfqRevisionService {
3
+ emFactory;
4
+ constructor(emFactory) {
5
+ this.emFactory = emFactory;
6
+ }
7
+ async record(input) {
8
+ // command-coverage-ignore: immutable per-revision snapshot of the RFQ state
9
+ // (version history), written as part of the audited parent RFQ action.
10
+ const em = this.emFactory();
11
+ const itemsSnapshot = input.items.map(toSnapshotLine);
12
+ // Caller sets `rfq.currentRevisionNumber` to the number of the
13
+ // revision being recorded BEFORE calling record(). This service
14
+ // never auto-bumps so the number on the rfq row and on the
15
+ // revision row are guaranteed to match.
16
+ const rev = em.create(QuoteRequestRevision, {
17
+ quoteRequestId: input.rfq.id,
18
+ revisionNumber: input.rfq.currentRevisionNumber,
19
+ createdByAdminUserId: input.actor.adminUserId ?? null,
20
+ createdByCustomerAccountId: input.actor.customerAccountId ?? null,
21
+ headerNoteSnapshot: input.rfq.headerNote ?? null,
22
+ itemsSnapshot,
23
+ previousRevisionId: input.previousRevisionId ?? null,
24
+ });
25
+ await em.persistAndFlush(rev);
26
+ return rev;
27
+ }
28
+ /** Returns the latest revision for an RFQ, if any. */
29
+ async latestForRfq(quoteRequestId) {
30
+ const em = this.emFactory();
31
+ const found = await em.find(QuoteRequestRevision, { quoteRequestId }, { orderBy: { revisionNumber: 'desc' }, limit: 1 });
32
+ return found[0] ?? null;
33
+ }
34
+ /** Returns the revision whose `revisionNumber === n`, if any. */
35
+ async byNumber(quoteRequestId, n) {
36
+ const em = this.emFactory();
37
+ const found = await em.find(QuoteRequestRevision, { quoteRequestId, revisionNumber: n }, { limit: 1 });
38
+ return found[0] ?? null;
39
+ }
40
+ /**
41
+ * Pure diff calculator. Takes two snapshots (`before`, `after`) plus
42
+ * their header notes and returns the list of diff entries in the
43
+ * order header → line_added → line_removed → line_quantity →
44
+ * line_agreed_unit_price. Stable by productId so renderers can
45
+ * group/sort easily.
46
+ */
47
+ diffRevisions(args) {
48
+ const out = [];
49
+ if ((args.beforeHeaderNote ?? null) !== (args.afterHeaderNote ?? null)) {
50
+ out.push({
51
+ kind: 'header_note',
52
+ before: args.beforeHeaderNote ?? null,
53
+ after: args.afterHeaderNote ?? null,
54
+ });
55
+ }
56
+ const beforeByProduct = new Map(args.beforeItems.map((l) => [l.productId, l]));
57
+ const afterByProduct = new Map(args.afterItems.map((l) => [l.productId, l]));
58
+ for (const [productId, after] of afterByProduct) {
59
+ if (!beforeByProduct.has(productId)) {
60
+ out.push({
61
+ kind: 'line_added',
62
+ productId,
63
+ productName: after.productName,
64
+ quantity: after.quantity,
65
+ agreedUnitPrice: after.agreedUnitPrice,
66
+ });
67
+ }
68
+ }
69
+ for (const [productId, before] of beforeByProduct) {
70
+ if (!afterByProduct.has(productId)) {
71
+ out.push({
72
+ kind: 'line_removed',
73
+ productId,
74
+ productName: before.productName,
75
+ });
76
+ }
77
+ }
78
+ for (const [productId, after] of afterByProduct) {
79
+ const before = beforeByProduct.get(productId);
80
+ if (!before)
81
+ continue;
82
+ if (before.quantity !== after.quantity) {
83
+ out.push({
84
+ kind: 'line_quantity',
85
+ productId,
86
+ productName: after.productName,
87
+ before: before.quantity,
88
+ after: after.quantity,
89
+ });
90
+ }
91
+ const beforePrice = before.agreedUnitPrice;
92
+ const afterPrice = after.agreedUnitPrice;
93
+ if (beforePrice !== afterPrice) {
94
+ out.push({
95
+ kind: 'line_agreed_unit_price',
96
+ productId,
97
+ productName: after.productName,
98
+ before: beforePrice,
99
+ after: afterPrice,
100
+ });
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ }
106
+ function toSnapshotLine(item) {
107
+ return {
108
+ productId: item.productId,
109
+ variantId: item.variantId ?? null,
110
+ productName: item.productName,
111
+ productSlug: item.productSlug ?? null,
112
+ quantity: item.quantity,
113
+ desiredUnitPrice: item.desiredUnitPrice != null ? Number(item.desiredUnitPrice) : null,
114
+ agreedUnitPrice: item.agreedUnitPrice != null ? Number(item.agreedUnitPrice) : null,
115
+ lineNote: item.lineNote ?? null,
116
+ lineCurrency: item.lineCurrency,
117
+ discountPercent: item.discountPercent != null ? Number(item.discountPercent) : null,
118
+ };
119
+ }
120
+ //# sourceMappingURL=rfq-revision-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfq-revision-service.js","sourceRoot":"","sources":["../../../src/backend/services/rfq-revision-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type QuoteRequest as RfqDto, type QuoteRequestSummary, type CreateQuoteRequest, type PatchQuoteRequest, type ResubmitQuoteRequest, type AdminUserReadPort, type CartWritePort, type CatalogProductReadPort, type CustomerAccountReadPort, type SalesRepAssignmentPort } from '@endora-commerce/contracts';
3
+ import type { EventBase, EventBus } from '@endora-commerce/platform/events';
4
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
5
+ import { QuoteRequest } from '../entities/quote-request.entity.js';
6
+ import type { RfqEventService } from './rfq-event-service.js';
7
+ import type { RfqRevisionService } from './rfq-revision-service.js';
8
+ import type { RfqNotificationService } from './rfq-notification-service.js';
9
+ import type { QuoteRequestBusinessIdGenerator } from './quote-request-business-id-generator.js';
10
+ import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
11
+ /**
12
+ * Customer-facing Quote Requests service — feature 008 workflow.
13
+ *
14
+ * Lifecycle methods exposed (each maps to one HTTP route):
15
+ * - list / getById (US1)
16
+ * - create + submit (US1)
17
+ * - patchDraft (US1, customer-side edit before any internal-side action)
18
+ * - resubmit (US1, "Submit again")
19
+ * - acceptRevision / rejectRevision (US3 — also covers Created from admin per US4)
20
+ * - convertToOrder (US5)
21
+ *
22
+ * The service writes through RfqEventService, RfqRevisionService, and
23
+ * RfqNotificationService for every state change so audit + comparison
24
+ * + notifications are consistent across all entry points.
25
+ */
26
+ export interface RfqEvents extends Record<string, EventBase> {
27
+ 'rfq.created.v1': EventBase & {
28
+ rfqId: string;
29
+ organizationId: string;
30
+ };
31
+ 'rfq.approved.v1': EventBase & {
32
+ rfqId: string;
33
+ };
34
+ 'rfq.canceled.v1': EventBase & {
35
+ rfqId: string;
36
+ reason: string | null;
37
+ };
38
+ 'rfq.modified.v1': EventBase & {
39
+ rfqId: string;
40
+ revisionNumber: number;
41
+ };
42
+ }
43
+ export type RfqEventBus = EventBus<RfqEvents>;
44
+ export interface CustomerContext {
45
+ customerAccountId: string;
46
+ organizationId: string;
47
+ /** True when the customer account holds the org-admin role on the
48
+ * current organization (FR-011 — broader visibility). */
49
+ isOrgAdmin: boolean;
50
+ }
51
+ export interface RfqServiceDeps {
52
+ emFactory: () => EntityManager;
53
+ events: RfqEventBus;
54
+ eventService: RfqEventService;
55
+ revisionService: RfqRevisionService;
56
+ notificationService: RfqNotificationService;
57
+ salesRepAssignment: SalesRepAssignmentPort;
58
+ /**
59
+ * Feature 075, Phase C — the four rows this service reads and none of which
60
+ * it owns, plus the one it writes.
61
+ *
62
+ * Every one of them was an `em.find` / `em.create` against another module's
63
+ * table, which is the shape Principle XVII cannot gate: deactivation drops no
64
+ * tables, so a quote kept resolving product names, requester identities and
65
+ * admin recipients — and kept seeding a cart — out of modules an operator had
66
+ * switched off. All four owners are binding `dependencies` of this manifest
67
+ * and every read fails closed; a quote priced against a catalogue the
68
+ * platform is not serving is worse than a refused quote.
69
+ *
70
+ * `carts` is the write, and it is the one that mattered most:
71
+ * `convertToOrder` created a `Cart` and hand-built `CartItem` rows, with the
72
+ * clear-then-seed rule spelled out here and `lastActivityAt` maintained
73
+ * nowhere. `replaceItemsForCustomer` is the port `carts` published for
74
+ * exactly this — its own doc comment names this call site — so the operation
75
+ * is owned end to end by the module that owns the tables (D-78 rule 1).
76
+ */
77
+ catalogProducts: CatalogProductReadPort;
78
+ /**
79
+ * The sanctioned bridge accessor (Constitution XII), for the assortment gate
80
+ * a quote line owes (issue #259). A kernel registration rather than a
81
+ * module's, so it is not one of the four edges the block above declares.
82
+ */
83
+ channelMembership: SalesChannelMembershipPort;
84
+ customerAccounts: CustomerAccountReadPort;
85
+ adminUsers: AdminUserReadPort;
86
+ carts: CartWritePort;
87
+ /**
88
+ * Generates the customer-facing business Quote Request ID. Optional so
89
+ * legacy/test compositions that don't wire it fall back to the entity's
90
+ * placeholder default.
91
+ */
92
+ businessId?: QuoteRequestBusinessIdGenerator;
93
+ /**
94
+ * Resolves the VAT rate (fraction, e.g. `0.23`) applied to the quote's net
95
+ * prices for the given Organization. Mirrors the Orders flow.
96
+ *
97
+ * Required since issue #124 — see `QuoteRequestsModuleOptions.resolveTaxRate`.
98
+ */
99
+ resolveTaxRate: (organizationId: string) => Promise<number>;
100
+ /** Feature 054 — audits RFQ writes co-transactionally when provided. */
101
+ auditLog?: AuditPort;
102
+ }
103
+ export declare class RfqService {
104
+ #private;
105
+ private readonly deps;
106
+ constructor(deps: RfqServiceDeps);
107
+ /**
108
+ * Draws the next business Quote Request ID, or `undefined` when no generator
109
+ * is wired (so the entity placeholder default applies). Shared with
110
+ * RfqAdminService so admin-created RFQs are numbered identically.
111
+ */
112
+ generateBusinessId(em: EntityManager): Promise<string | undefined>;
113
+ /**
114
+ * Resolves the VAT rate applied to a quote's net prices for an Organization.
115
+ *
116
+ * No unwired branch (issue #124): the resolver is required, and an absent
117
+ * `taxes` module surfaces through it as `MODULE_DISABLED` rather than as a
118
+ * quote silently priced at 0% VAT.
119
+ */
120
+ taxRateForOrganization(organizationId: string): Promise<number>;
121
+ listForCustomer(ctx: CustomerContext): Promise<QuoteRequestSummary[]>;
122
+ getForCustomer(rfqId: string, ctx: CustomerContext): Promise<RfqDto>;
123
+ createForCustomer(ctx: CustomerContext, input: CreateQuoteRequest): Promise<RfqDto>;
124
+ patchDraft(rfqId: string, ctx: CustomerContext, body: PatchQuoteRequest, expectedVersion: number | null): Promise<RfqDto>;
125
+ resubmit(rfqId: string, ctx: CustomerContext, body: ResubmitQuoteRequest): Promise<RfqDto>;
126
+ acceptRevision(rfqId: string, ctx: CustomerContext, expectedRevisionNumber: number): Promise<RfqDto>;
127
+ rejectRevision(rfqId: string, ctx: CustomerContext, expectedRevisionNumber: number, reason: string | undefined): Promise<RfqDto>;
128
+ private respondToRevision;
129
+ convertToOrder(rfqId: string, ctx: CustomerContext): Promise<{
130
+ cartId: string;
131
+ checkoutUrl: string;
132
+ }>;
133
+ private findVisibleForCustomer;
134
+ private notifyInternalSide;
135
+ /** Serialise to the customer-facing response shape. Hides internal admin
136
+ * actor identity behind `actorRoleLabel` per FR-037 unless
137
+ * `includeFullActorIdentity = true` (admin path). */
138
+ serializeFull(em: EntityManager, rfq: QuoteRequest, includeFullActorIdentity: boolean): Promise<RfqDto>;
139
+ }
140
+ //# sourceMappingURL=rfq-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rfq-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/rfq-service.ts"],"names":[],"mappings":"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"}