@mohasinac/appkit 4.62.0 → 4.62.1

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 (204) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
  3. package/dist/_internal/server/features/checkout/actions.js +542 -26
  4. package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
  5. package/dist/_internal/server/features/invoicing/allocate.js +162 -0
  6. package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
  7. package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
  8. package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
  9. package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
  10. package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
  11. package/dist/_internal/server/features/invoicing/issue.js +202 -0
  12. package/dist/_internal/server/features/orders/actions.d.ts +8 -1
  13. package/dist/_internal/server/features/orders/actions.js +81 -8
  14. package/dist/_internal/server/features/orders/adapters.js +33 -0
  15. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
  16. package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
  17. package/dist/_internal/server/features/points/award.js +46 -4
  18. package/dist/_internal/server/features/refunds/actions.js +26 -0
  19. package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
  20. package/dist/_internal/server/features/shipping/courier-block.js +239 -0
  21. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  22. package/dist/_internal/server/features/site-settings/adapters.js +15 -0
  23. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  24. package/dist/_internal/server/features/stores/adapters.js +51 -0
  25. package/dist/_internal/server/functions/scheduled.js +38 -1
  26. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
  27. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
  28. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
  29. package/dist/_internal/server/jobs/core/index.d.ts +2 -1
  30. package/dist/_internal/server/jobs/core/index.js +2 -1
  31. package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
  32. package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
  33. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
  34. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
  35. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
  36. package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
  37. package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
  38. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
  39. package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
  40. package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
  41. package/dist/_internal/shared/actions/action-registry.js +28 -0
  42. package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
  43. package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
  44. package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
  45. package/dist/_internal/shared/checkout/order-math.js +173 -0
  46. package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
  47. package/dist/_internal/shared/checkout/refundable-base.js +13 -1
  48. package/dist/_internal/shared/checkout/split.d.ts +87 -0
  49. package/dist/_internal/shared/checkout/split.js +100 -0
  50. package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
  51. package/dist/_internal/shared/content/disclaimers.js +217 -0
  52. package/dist/_internal/shared/content/published-values.d.ts +88 -0
  53. package/dist/_internal/shared/content/published-values.js +198 -0
  54. package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
  55. package/dist/_internal/shared/features/admin/integrations.js +253 -0
  56. package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
  57. package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
  58. package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
  59. package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
  60. package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
  61. package/dist/_internal/shared/features/points/accepts.js +43 -0
  62. package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
  63. package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
  64. package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
  65. package/dist/_internal/shared/fees/calculator.d.ts +33 -1
  66. package/dist/_internal/shared/fees/calculator.js +32 -2
  67. package/dist/client.d.ts +11 -0
  68. package/dist/client.js +30 -0
  69. package/dist/constants/api-endpoints.d.ts +42 -0
  70. package/dist/constants/api-endpoints.js +14 -0
  71. package/dist/constants/field-names.d.ts +1 -0
  72. package/dist/constants/field-names.js +5 -0
  73. package/dist/contracts/index.d.ts +1 -1
  74. package/dist/contracts/shipping.d.ts +45 -0
  75. package/dist/contracts/shipping.js +15 -0
  76. package/dist/features/about/components/AboutView.js +1 -1
  77. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
  78. package/dist/features/account/components/UserOrdersView.js +19 -33
  79. package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
  80. package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
  81. package/dist/features/admin/components/AdminInvoicingView.js +65 -0
  82. package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
  83. package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
  84. package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
  85. package/dist/features/admin/components/AdminOrdersView.js +67 -5
  86. package/dist/features/admin/components/OwnerQuickActions.js +24 -2
  87. package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
  88. package/dist/features/admin/constants/filter-tabs.js +6 -0
  89. package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
  90. package/dist/features/admin/hooks/useAdminListingData.js +16 -0
  91. package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
  92. package/dist/features/admin/schemas/firestore.d.ts +30 -0
  93. package/dist/features/admin/schemas/firestore.js +5 -0
  94. package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
  95. package/dist/features/audit-log/schemas/firestore.js +15 -0
  96. package/dist/features/auth/permissions/constants.js +13 -0
  97. package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
  98. package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
  99. package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
  100. package/dist/features/cart/components/PointsTenderPicker.js +15 -0
  101. package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
  102. package/dist/features/cart/components/index.d.ts +2 -0
  103. package/dist/features/cart/components/index.js +1 -0
  104. package/dist/features/cart/repository/cart.repository.d.ts +19 -0
  105. package/dist/features/cart/repository/cart.repository.js +27 -0
  106. package/dist/features/cart/schemas/firestore.d.ts +92 -0
  107. package/dist/features/contact/email.d.ts +27 -10
  108. package/dist/features/contact/email.js +20 -3
  109. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
  110. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
  111. package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
  112. package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
  113. package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
  114. package/dist/features/faq/components/FAQAccordion.js +29 -4
  115. package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
  116. package/dist/features/invoicing/schemas/firestore.js +45 -0
  117. package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
  118. package/dist/features/invoicing/utils/financial-year.js +53 -0
  119. package/dist/features/jobs/actions/enqueue-job.js +18 -1
  120. package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
  121. package/dist/features/jobs/repository/jobs.repository.js +84 -1
  122. package/dist/features/jobs/schemas/firestore.d.ts +80 -0
  123. package/dist/features/jobs/schemas/firestore.js +37 -0
  124. package/dist/features/lottery/schemas/zod.d.ts +6 -6
  125. package/dist/features/media/MediaImage.js +22 -1
  126. package/dist/features/media/upload/ImageUpload.js +1 -1
  127. package/dist/features/obligations/constants/policies.js +24 -1
  128. package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
  129. package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
  130. package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
  131. package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
  132. package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
  133. package/dist/features/orders/components/RefundRequestView.js +19 -1
  134. package/dist/features/orders/components/index.d.ts +4 -0
  135. package/dist/features/orders/components/index.js +2 -0
  136. package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
  137. package/dist/features/orders/constants/order-lane-tabs.js +88 -0
  138. package/dist/features/orders/repository/orders.repository.d.ts +57 -0
  139. package/dist/features/orders/repository/orders.repository.js +145 -0
  140. package/dist/features/orders/schemas/firestore.d.ts +196 -1
  141. package/dist/features/orders/schemas/index.d.ts +54 -6
  142. package/dist/features/orders/schemas/index.js +12 -0
  143. package/dist/features/orders/types/index.d.ts +26 -0
  144. package/dist/features/orders/utils/courier-block.d.ts +25 -0
  145. package/dist/features/orders/utils/courier-block.js +23 -0
  146. package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
  147. package/dist/features/payment-cases/schemas/firestore.js +1 -0
  148. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  149. package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
  150. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  151. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  152. package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
  153. package/dist/features/promotions/actions/coupon-stacking.js +21 -2
  154. package/dist/features/promotions/components/CouponCard.js +13 -0
  155. package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
  156. package/dist/features/promotions/constants/coupon-help.js +9 -1
  157. package/dist/features/promotions/schemas/firestore.d.ts +24 -0
  158. package/dist/features/promotions/schemas/firestore.js +4 -0
  159. package/dist/features/promotions/types/index.d.ts +19 -1
  160. package/dist/features/scams/components/ScamProfileView.js +2 -2
  161. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  162. package/dist/features/search/action-index/tab-entries.js +1 -1
  163. package/dist/features/seller/actions/seller-actions.js +33 -3
  164. package/dist/features/seller/components/SellerOrdersView.js +129 -27
  165. package/dist/features/seller/hooks/useSellerListingData.js +25 -0
  166. package/dist/features/shipments/schemas/validation.d.ts +4 -4
  167. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
  168. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
  169. package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
  170. package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
  171. package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
  172. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
  173. package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
  174. package/dist/features/stores/schemas/firestore.d.ts +71 -0
  175. package/dist/features/stores/types/index.d.ts +19 -0
  176. package/dist/features/support/schemas/index.d.ts +6 -6
  177. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  178. package/dist/index.d.ts +6 -1
  179. package/dist/index.js +14 -1
  180. package/dist/next/routing/route-map.d.ts +20 -0
  181. package/dist/next/routing/route-map.js +10 -0
  182. package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
  183. package/dist/providers/shipping-shiprocket/index.js +84 -1
  184. package/dist/schemas/registry.d.ts +47 -18
  185. package/dist/schemas/sieve.d.ts +2 -2
  186. package/dist/server-entry.d.ts +2 -2
  187. package/dist/server-entry.js +8 -1
  188. package/dist/server.d.ts +10 -1
  189. package/dist/server.js +39 -1
  190. package/dist/styles.css +64 -80
  191. package/dist/tailwind-utilities.css +1 -1
  192. package/dist/tokens/themes/default-dark.js +1 -0
  193. package/dist/tokens/themes/default-light.js +1 -0
  194. package/dist/tokens/themes/required.d.ts +1 -1
  195. package/dist/tokens/themes/required.js +1 -0
  196. package/dist/tokens/tokens.css +15 -0
  197. package/dist/ui/components/Disclaimer.d.ts +19 -0
  198. package/dist/ui/components/Disclaimer.js +74 -0
  199. package/dist/ui/components/tabs-overflow.js +64 -11
  200. package/dist/ui/index.d.ts +2 -0
  201. package/dist/ui/index.js +6 -0
  202. package/dist/ui/rich-text/RichText.style.css +48 -79
  203. package/package.json +1 -1
  204. package/scripts/seed-cli.mjs +1831 -1822
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Invoicing — the two gapless series (Phase 7, D12)
3
+ *
4
+ * ## Why two series and not one field
5
+ *
6
+ * **A GST invoice is issued once and must not change.** So a partial
7
+ * cancellation or refund gets a CREDIT NOTE with its own number referencing
8
+ * the invoice it adjusts — never a re-issued invoice. That is the whole reason
9
+ * `creditNoteNumber` is a second series rather than a revision suffix.
10
+ *
11
+ * ## 🛑 Gapless means the counter CANNOT be sharded
12
+ *
13
+ * Firestore's ~1 write/sec/document ceiling applies to a counter doc, and the
14
+ * usual answer is to shard it. That answer is unavailable here: a sharded
15
+ * counter cannot be gapless, which is the entire requirement. At this volume
16
+ * the ceiling is irrelevant; the note exists so nobody "optimises" it later
17
+ * and silently breaks the one property an auditor checks.
18
+ *
19
+ * ## 🛑 The FINANCIAL YEAR is the riskiest line in the phase
20
+ *
21
+ * The series is `LIR/26-27/0001` (decision 21), resetting on 1 April. So the
22
+ * allocating transaction must key on the FY — if it keys on anything else, two
23
+ * years share one counter and NEITHER series is gapless. And a number cannot
24
+ * be withdrawn without leaving a gap, so this is not a mistake that can be
25
+ * corrected after the fact.
26
+ */
27
+ /**
28
+ * ## 🛑 OUTSTANDING — both collections need a PRESERVE entry in the tester tier
29
+ *
30
+ * `invoiceCounters` and `invoiceAllocations` are in **no tier**, and
31
+ * `assertDeletable` THROWS on an unlisted collection name *mid-run* — which
32
+ * reads as a harness fault rather than a classification gap. That is the exact
33
+ * latent trip `gatewayOrderClaims` sat on, and these two are worse: a wiped
34
+ * counter would restart a series at 1 and re-issue numbers that already exist
35
+ * on real invoices, which is unrecoverable.
36
+ *
37
+ * The entry belongs in `tester/scripts/lib/collections.mjs`'s PRESERVE list,
38
+ * beside `payoutMethods` and `adminAuditLog`. It is not in this change because
39
+ * **the `tester` submodule is not initialised in this worktree** (`git
40
+ * submodule status` shows it uninitialised), so the file does not exist here to
41
+ * edit. Recorded rather than silently skipped.
42
+ *
43
+ * 🛑 And they must get **no `SEED_COLLECTION_PURPOSE` entry**:
44
+ * `audit-seed-purpose` R1 requires that registry to match `COLLECTION_MAP` in
45
+ * BOTH directions, so a key for a collection the seeder does not write is a
46
+ * DEAD entry and fails the audit. Neither of these is ever seeded — they are
47
+ * written only by allocation. The same mistake was made with `refundClaims`
48
+ * and reverted.
49
+ */
50
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
51
+ export declare const INVOICE_COUNTER_COLLECTION = "invoiceCounters";
52
+ /** The two independent series. */
53
+ export type InvoiceSeriesKind = "invoice" | "credit_note";
54
+ /**
55
+ * One counter per (series, financial year).
56
+ *
57
+ * The document id IS `${kind}_${fy}` — e.g. `invoice_26-27` — so the
58
+ * transaction that allocates a number needs no query, and two series in the
59
+ * same year cannot contend with each other.
60
+ */
61
+ export interface InvoiceCounterDocument extends BaseDocument {
62
+ kind: InvoiceSeriesKind;
63
+ /** `"26-27"` — the FY label that appears in the number itself. */
64
+ fy: string;
65
+ /**
66
+ * The last number ISSUED. The next allocation is `lastNumber + 1`.
67
+ *
68
+ * 🛑 Last-issued rather than next-to-issue, deliberately: a counter created
69
+ * at 0 means "nothing issued yet", which is true and self-evident, whereas a
70
+ * next-to-issue counter created at 0 would mint `LIR/26-27/0000`.
71
+ */
72
+ lastNumber: number;
73
+ /** When this counter last allocated — for the operator, never read by code. */
74
+ lastIssuedAt?: Date;
75
+ }
76
+ /**
77
+ * A record of every number allocated, keyed on what it was allocated FOR.
78
+ *
79
+ * ## 🛑 This is the idempotency claim, and it is why a number cannot be issued
80
+ * twice for one order
81
+ *
82
+ * `onOrderStatusChange` can fire more than once, and **two numbers for one
83
+ * order is the single unrecoverable failure in this phase** — the spare number
84
+ * is a permanent gap that cannot be explained to an auditor and cannot be
85
+ * reclaimed.
86
+ *
87
+ * So allocation is a `.create()` on `invoiceAllocations/{subjectId}` INSIDE
88
+ * the same transaction that bumps the counter. `.create()` fails if the
89
+ * document exists, which makes "already allocated" a transaction-level fact
90
+ * rather than a read-then-write race. It is the same primitive the refund
91
+ * claim, the gateway-event inbox and the gateway-order claim all use.
92
+ */
93
+ export declare const INVOICE_ALLOCATION_COLLECTION = "invoiceAllocations";
94
+ export interface InvoiceAllocationDocument extends BaseDocument {
95
+ kind: InvoiceSeriesKind;
96
+ /**
97
+ * What this number was issued for.
98
+ *
99
+ * 🛑 For an invoice this is the ORDER id, so one order can only ever hold
100
+ * one invoice number. For a credit note it is `${orderId}_${refundId}`,
101
+ * because an order can legitimately have several credit notes — one per
102
+ * refund event — and keying a credit note on the order alone would refuse
103
+ * the second one.
104
+ */
105
+ subjectId: string;
106
+ orderId: string;
107
+ /** The formatted number, e.g. `LIR/26-27/0001`. */
108
+ number: string;
109
+ fy: string;
110
+ sequence: number;
111
+ issuedAt: Date;
112
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Invoicing — the two gapless series (Phase 7, D12)
3
+ *
4
+ * ## Why two series and not one field
5
+ *
6
+ * **A GST invoice is issued once and must not change.** So a partial
7
+ * cancellation or refund gets a CREDIT NOTE with its own number referencing
8
+ * the invoice it adjusts — never a re-issued invoice. That is the whole reason
9
+ * `creditNoteNumber` is a second series rather than a revision suffix.
10
+ *
11
+ * ## 🛑 Gapless means the counter CANNOT be sharded
12
+ *
13
+ * Firestore's ~1 write/sec/document ceiling applies to a counter doc, and the
14
+ * usual answer is to shard it. That answer is unavailable here: a sharded
15
+ * counter cannot be gapless, which is the entire requirement. At this volume
16
+ * the ceiling is irrelevant; the note exists so nobody "optimises" it later
17
+ * and silently breaks the one property an auditor checks.
18
+ *
19
+ * ## 🛑 The FINANCIAL YEAR is the riskiest line in the phase
20
+ *
21
+ * The series is `LIR/26-27/0001` (decision 21), resetting on 1 April. So the
22
+ * allocating transaction must key on the FY — if it keys on anything else, two
23
+ * years share one counter and NEITHER series is gapless. And a number cannot
24
+ * be withdrawn without leaving a gap, so this is not a mistake that can be
25
+ * corrected after the fact.
26
+ */
27
+ export const INVOICE_COUNTER_COLLECTION = "invoiceCounters";
28
+ /**
29
+ * A record of every number allocated, keyed on what it was allocated FOR.
30
+ *
31
+ * ## 🛑 This is the idempotency claim, and it is why a number cannot be issued
32
+ * twice for one order
33
+ *
34
+ * `onOrderStatusChange` can fire more than once, and **two numbers for one
35
+ * order is the single unrecoverable failure in this phase** — the spare number
36
+ * is a permanent gap that cannot be explained to an auditor and cannot be
37
+ * reclaimed.
38
+ *
39
+ * So allocation is a `.create()` on `invoiceAllocations/{subjectId}` INSIDE
40
+ * the same transaction that bumps the counter. `.create()` fails if the
41
+ * document exists, which makes "already allocated" a transaction-level fact
42
+ * rather than a read-then-write race. It is the same primitive the refund
43
+ * claim, the gateway-event inbox and the gateway-order claim all use.
44
+ */
45
+ export const INVOICE_ALLOCATION_COLLECTION = "invoiceAllocations";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Indian financial year, and the series format built on it.
3
+ *
4
+ * ## 🛑 1 April, and getting the boundary wrong is unrecoverable
5
+ *
6
+ * The FY segment is what makes the reset safe: a counter keyed on the year
7
+ * resets by construction, because 1 April produces a new key. But it also
8
+ * means a boundary error is permanent — a number issued under the wrong FY
9
+ * leaves a gap in one year's series and an extra in the other, and a number
10
+ * cannot be withdrawn.
11
+ *
12
+ * So the month test is `>= 3` on a ZERO-BASED month (April is 3, not 4). That
13
+ * single off-by-one is the whole risk, and it is why this is a named function
14
+ * with its reasoning attached rather than an inline expression at the call
15
+ * site.
16
+ */
17
+ /**
18
+ * `"26-27"` for any date from 2026-04-01 to 2027-03-31.
19
+ *
20
+ * Two-digit years, matching decision 21's `LIR/26-27/0001`. The full number is
21
+ * 14 characters, inside the statutory 16, and uses only `-` and `/` as
22
+ * separators.
23
+ */
24
+ export declare function financialYearLabel(at: Date): string;
25
+ /**
26
+ * Prefixes, per decision 21.
27
+ *
28
+ * 🛑 Distinct prefixes rather than one with a suffix, so the two series are
29
+ * distinguishable at a glance in a bank statement, a spreadsheet and a GSTR-1
30
+ * row — and so a text search for one can never match the other.
31
+ */
32
+ export declare const INVOICE_PREFIX = "LIR";
33
+ export declare const CREDIT_NOTE_PREFIX = "LIRCN";
34
+ /**
35
+ * `LIR/26-27/0001`.
36
+ *
37
+ * Zero-padded to four digits, which covers 9,999 documents per series per
38
+ * year. 🛑 Overflow does NOT wrap or throw: `padStart` is a floor, not a
39
+ * ceiling, so number 10,000 renders as `10000` and the series stays gapless
40
+ * and sortable-by-sequence. Width is cosmetic; gaplessness is not, and the
41
+ * failure mode of a throw here would be a paid order that cannot be invoiced.
42
+ */
43
+ export declare function formatSeriesNumber(prefix: string, fy: string, sequence: number): string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The Indian financial year, and the series format built on it.
3
+ *
4
+ * ## 🛑 1 April, and getting the boundary wrong is unrecoverable
5
+ *
6
+ * The FY segment is what makes the reset safe: a counter keyed on the year
7
+ * resets by construction, because 1 April produces a new key. But it also
8
+ * means a boundary error is permanent — a number issued under the wrong FY
9
+ * leaves a gap in one year's series and an extra in the other, and a number
10
+ * cannot be withdrawn.
11
+ *
12
+ * So the month test is `>= 3` on a ZERO-BASED month (April is 3, not 4). That
13
+ * single off-by-one is the whole risk, and it is why this is a named function
14
+ * with its reasoning attached rather than an inline expression at the call
15
+ * site.
16
+ */
17
+ /**
18
+ * `"26-27"` for any date from 2026-04-01 to 2027-03-31.
19
+ *
20
+ * Two-digit years, matching decision 21's `LIR/26-27/0001`. The full number is
21
+ * 14 characters, inside the statutory 16, and uses only `-` and `/` as
22
+ * separators.
23
+ */
24
+ export function financialYearLabel(at) {
25
+ const year = at.getFullYear();
26
+ // 🛑 ZERO-BASED. `getMonth()` returns 3 for April, so `>= 3` is "April or
27
+ // later" and `< 3` is "January to March", which belongs to the FY that
28
+ // STARTED in the previous calendar year.
29
+ const startYear = at.getMonth() >= 3 ? year : year - 1;
30
+ const endYear = startYear + 1;
31
+ return `${String(startYear % 100).padStart(2, "0")}-${String(endYear % 100).padStart(2, "0")}`;
32
+ }
33
+ /**
34
+ * Prefixes, per decision 21.
35
+ *
36
+ * 🛑 Distinct prefixes rather than one with a suffix, so the two series are
37
+ * distinguishable at a glance in a bank statement, a spreadsheet and a GSTR-1
38
+ * row — and so a text search for one can never match the other.
39
+ */
40
+ export const INVOICE_PREFIX = "LIR";
41
+ export const CREDIT_NOTE_PREFIX = "LIRCN";
42
+ /**
43
+ * `LIR/26-27/0001`.
44
+ *
45
+ * Zero-padded to four digits, which covers 9,999 documents per series per
46
+ * year. 🛑 Overflow does NOT wrap or throw: `padStart` is a floor, not a
47
+ * ceiling, so number 10,000 renders as `10000` and the series stays gapless
48
+ * and sortable-by-sequence. Width is cosmetic; gaplessness is not, and the
49
+ * failure mode of a throw here would be a paid order that cannot be invoiced.
50
+ */
51
+ export function formatSeriesNumber(prefix, fy, sequence) {
52
+ return `${prefix}/${fy}/${String(sequence).padStart(4, "0")}`;
53
+ }
@@ -15,7 +15,7 @@
15
15
  import { normalizeError } from "../../../errors/normalize.js";
16
16
  import { serverLogger } from "../../../monitoring/index.js";
17
17
  import { jobsRepository } from "../../../repositories/index.js";
18
- import { JobStatusValues } from "../schemas/firestore.js";
18
+ import { JOB_MAX_ATTEMPTS, JobStatusValues } from "../schemas/firestore.js";
19
19
  import { getAdminAuth, getAdminRealtimeDb } from "../../../providers/db-firebase/index.js";
20
20
  import { RTDB_PATHS } from "../../../providers/db-firebase/rtdb-paths.js";
21
21
  export async function enqueueJob(input) {
@@ -24,6 +24,23 @@ export async function enqueueJob(input) {
24
24
  status: JobStatusValues.PENDING,
25
25
  payload: input.payload,
26
26
  requestedBy: input.requestedBy,
27
+ /*
28
+ * Phase 8 — attempt 1 of N, stated rather than implied.
29
+ *
30
+ * `attempt` defaults to 1 everywhere it is read, so this write is not
31
+ * strictly required — but an explicit 1 is what makes an ENQUEUED job
32
+ * distinguishable from a pre-Phase-8 row at a glance, and it is what a
33
+ * reader of the collection needs when three documents share a
34
+ * `parentJobId`.
35
+ *
36
+ * 🛑 `nextAttemptAt` is deliberately ABSENT. It is the retry sweep's query
37
+ * field, and the create trigger skips any job carrying a future one — so
38
+ * setting it here (even to `now`) would put every first attempt at the
39
+ * mercy of a 5-minute sweep instead of running immediately, turning a
40
+ * synchronous-feeling admin action into one that appears to hang.
41
+ */
42
+ attempt: 1,
43
+ maxAttempts: JOB_MAX_ATTEMPTS,
27
44
  });
28
45
  try {
29
46
  await getAdminRealtimeDb()
@@ -13,8 +13,69 @@ export declare class JobsRepository extends BaseRepository<JobDocument> {
13
13
  markDone(id: string, result: JobResult): Promise<JobDocument>;
14
14
  markFailed(id: string, error: string): Promise<JobDocument>;
15
15
  /**
16
- * Refs for finished (done/failed) jobs older than `ttlDays` — used by the
16
+ * Attempts exhausted, or a permanent failure. The dead-letter state
17
+ * (Phase 8).
18
+ *
19
+ * 🛑 Distinct from `markFailed`, which now means "this attempt failed and a
20
+ * retry is scheduled". Before Phase 8 there was only `failed`, and it was
21
+ * terminal with no signal — so a job that gave up looked exactly like a job
22
+ * that had just failed once, and nothing anywhere distinguished them.
23
+ */
24
+ markDead(id: string, error: string): Promise<JobDocument>;
25
+ /**
26
+ * Schedule the next attempt as a NEW document (Phase 8).
27
+ *
28
+ * ## 🛑 Why a new document and not a reset of this one
29
+ *
30
+ * `onJobCreated` fires on CREATE. Flipping a job back to `pending` would
31
+ * never wake the trigger, so it would sit `pending` forever looking
32
+ * scheduled — a stall that reads as a queue. A fresh document is what makes
33
+ * the retry visible to the only mechanism that can run it.
34
+ *
35
+ * It also keeps the history: three attempts are three documents, each with
36
+ * its own error, so a repeating failure is legible instead of being
37
+ * overwritten by the last one.
38
+ *
39
+ * `parentJobId` points at the ORIGINAL — `parentJobId ?? id`, so attempt 3
40
+ * is linked to attempt 1 rather than to attempt 2. A flat fan means "every
41
+ * attempt of this job" is one equality query, where a linked list needs N
42
+ * reads to walk.
43
+ */
44
+ createRetry(id: string, job: JobDocument, next: {
45
+ attempt: number;
46
+ maxAttempts: number;
47
+ nextAttemptAt: Date;
48
+ }): Promise<string>;
49
+ /**
50
+ * Retries whose `nextAttemptAt` has passed — the sweep's only query.
51
+ *
52
+ * 🛑 `status == pending` AND `nextAttemptAt <= now`, so it can never pick up
53
+ * a FIRST attempt: an original job carries no `nextAttemptAt`, and a
54
+ * Firestore inequality excludes every document that lacks the field. That
55
+ * exclusion is usually the trap (see `.where("isTestData","!=",true)` in
56
+ * CLAUDE.md, and D26's `orderBy` on an optional field); here it is exactly
57
+ * the filter wanted, and it is written down so nobody "fixes" it by adding
58
+ * a default.
59
+ *
60
+ * Bounded at 20 per tick. The sweep rides `paymentWindowTimeout`, which runs
61
+ * every 5 minutes, so that is 240 retries an hour — far above any real
62
+ * backlog, and low enough that a pathological queue cannot exhaust the
63
+ * function's own budget in one pass.
64
+ */
65
+ getDueRetries(limit?: number): Promise<Array<{
66
+ id: string;
67
+ data: JobDocument;
68
+ }>>;
69
+ /**
70
+ * Refs for finished jobs older than `ttlDays` — used by the
17
71
  * `cleanupRtdbEvents` sweep to prune the `jobs` collection.
72
+ *
73
+ * 🛑 `DEAD` is deliberately NOT in this list. A dead job has an open
74
+ * `dead_job` case pointing at it, and the job document holds the payload
75
+ * that would let someone re-run the work by hand — pruning it would leave a
76
+ * case referencing nothing, which is worse than a stale row. They are
77
+ * retained until a human resolves the case and can be pruned by a separate,
78
+ * case-aware sweep if they ever accumulate.
18
79
  */
19
80
  getStaleFinishedRefs(ttlDays?: number): Promise<DocumentReference[]>;
20
81
  }
@@ -31,8 +31,91 @@ export class JobsRepository extends BaseRepository {
31
31
  });
32
32
  }
33
33
  /**
34
- * Refs for finished (done/failed) jobs older than `ttlDays` — used by the
34
+ * Attempts exhausted, or a permanent failure. The dead-letter state
35
+ * (Phase 8).
36
+ *
37
+ * 🛑 Distinct from `markFailed`, which now means "this attempt failed and a
38
+ * retry is scheduled". Before Phase 8 there was only `failed`, and it was
39
+ * terminal with no signal — so a job that gave up looked exactly like a job
40
+ * that had just failed once, and nothing anywhere distinguished them.
41
+ */
42
+ markDead(id, error) {
43
+ return this.update(id, {
44
+ status: JobStatusValues.DEAD,
45
+ error,
46
+ finishedAt: new Date(),
47
+ });
48
+ }
49
+ /**
50
+ * Schedule the next attempt as a NEW document (Phase 8).
51
+ *
52
+ * ## 🛑 Why a new document and not a reset of this one
53
+ *
54
+ * `onJobCreated` fires on CREATE. Flipping a job back to `pending` would
55
+ * never wake the trigger, so it would sit `pending` forever looking
56
+ * scheduled — a stall that reads as a queue. A fresh document is what makes
57
+ * the retry visible to the only mechanism that can run it.
58
+ *
59
+ * It also keeps the history: three attempts are three documents, each with
60
+ * its own error, so a repeating failure is legible instead of being
61
+ * overwritten by the last one.
62
+ *
63
+ * `parentJobId` points at the ORIGINAL — `parentJobId ?? id`, so attempt 3
64
+ * is linked to attempt 1 rather than to attempt 2. A flat fan means "every
65
+ * attempt of this job" is one equality query, where a linked list needs N
66
+ * reads to walk.
67
+ */
68
+ async createRetry(id, job, next) {
69
+ const created = await this.create({
70
+ jobType: job.jobType,
71
+ status: JobStatusValues.PENDING,
72
+ payload: job.payload,
73
+ requestedBy: job.requestedBy,
74
+ attempt: next.attempt,
75
+ maxAttempts: next.maxAttempts,
76
+ nextAttemptAt: next.nextAttemptAt,
77
+ parentJobId: job.parentJobId ?? id,
78
+ lastError: job.error,
79
+ });
80
+ return created.id;
81
+ }
82
+ /**
83
+ * Retries whose `nextAttemptAt` has passed — the sweep's only query.
84
+ *
85
+ * 🛑 `status == pending` AND `nextAttemptAt <= now`, so it can never pick up
86
+ * a FIRST attempt: an original job carries no `nextAttemptAt`, and a
87
+ * Firestore inequality excludes every document that lacks the field. That
88
+ * exclusion is usually the trap (see `.where("isTestData","!=",true)` in
89
+ * CLAUDE.md, and D26's `orderBy` on an optional field); here it is exactly
90
+ * the filter wanted, and it is written down so nobody "fixes" it by adding
91
+ * a default.
92
+ *
93
+ * Bounded at 20 per tick. The sweep rides `paymentWindowTimeout`, which runs
94
+ * every 5 minutes, so that is 240 retries an hour — far above any real
95
+ * backlog, and low enough that a pathological queue cannot exhaust the
96
+ * function's own budget in one pass.
97
+ */
98
+ async getDueRetries(limit = 20) {
99
+ const snap = await this.getCollection()
100
+ .where(JOB_FIELDS.STATUS, "==", JobStatusValues.PENDING)
101
+ .where(JOB_FIELDS.NEXT_ATTEMPT_AT, "<=", new Date())
102
+ .limit(limit)
103
+ .get();
104
+ return snap.docs.map((d) => ({
105
+ id: d.id,
106
+ data: { id: d.id, ...d.data() },
107
+ }));
108
+ }
109
+ /**
110
+ * Refs for finished jobs older than `ttlDays` — used by the
35
111
  * `cleanupRtdbEvents` sweep to prune the `jobs` collection.
112
+ *
113
+ * 🛑 `DEAD` is deliberately NOT in this list. A dead job has an open
114
+ * `dead_job` case pointing at it, and the job document holds the payload
115
+ * that would let someone re-run the work by hand — pruning it would leave a
116
+ * case referencing nothing, which is worse than a stale row. They are
117
+ * retained until a human resolves the case and can be pruned by a separate,
118
+ * case-aware sweep if they ever accumulate.
36
119
  */
37
120
  async getStaleFinishedRefs(ttlDays = 30) {
38
121
  const cutoff = new Date();
@@ -23,6 +23,20 @@ export declare const JobStatusValues: {
23
23
  readonly PROCESSING: "processing";
24
24
  readonly DONE: "done";
25
25
  readonly FAILED: "failed";
26
+ /**
27
+ * Phase 8 — attempts exhausted. The dead-letter state.
28
+ *
29
+ * 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
30
+ * made a failed job terminal with no signal. `failed` now means "this
31
+ * attempt failed, and a retry is scheduled or the failure was permanent";
32
+ * `dead` means "we have stopped trying" and is the only one that opens a
33
+ * case.
34
+ *
35
+ * A reader that treats every non-`done` as hopeless loses that distinction —
36
+ * which is why the retention sweep excludes `dead` while an open case
37
+ * references it: the row is the evidence.
38
+ */
39
+ readonly DEAD: "dead";
26
40
  };
27
41
  export type JobStatus = (typeof JobStatusValues)[keyof typeof JobStatusValues];
28
42
  export interface JobResult {
@@ -52,7 +66,55 @@ export interface JobDocument extends BaseDocument {
52
66
  error?: string;
53
67
  startedAt?: Date;
54
68
  finishedAt?: Date;
69
+ /**
70
+ * 1-based. Absent means 1 — every job written before this field is a first
71
+ * attempt, which is both true and what every reader should assume.
72
+ */
73
+ attempt?: number;
74
+ /** Defaults to `JOB_MAX_ATTEMPTS`; a jobType may lower it. */
75
+ maxAttempts?: number;
76
+ /**
77
+ * When a retry becomes eligible.
78
+ *
79
+ * 🛑 A retry is a **NEW DOCUMENT**, not a mutation of this one, because
80
+ * `onJobCreated` fires on CREATE — re-running a job by flipping its status
81
+ * back to `pending` would never wake the trigger, and the job would sit
82
+ * `pending` forever looking scheduled. So this field lives on the NEW
83
+ * document and a sweep is what moves it from scheduled to running.
84
+ */
85
+ nextAttemptAt?: Date;
86
+ /**
87
+ * The document this is a retry OF — present on every attempt after the
88
+ * first, and absent on the original.
89
+ *
90
+ * 🛑 Points at the ORIGINAL, not at the immediately previous attempt. A
91
+ * flat chain means "show me every attempt of this job" is one equality
92
+ * query, where a linked list would need N reads to walk; and the question
93
+ * anyone actually asks is "did this job eventually work", which is about the
94
+ * original.
95
+ */
96
+ parentJobId?: string;
97
+ /** Why this attempt failed, kept per attempt so a pattern is visible. */
98
+ lastError?: string;
55
99
  }
100
+ /**
101
+ * Attempts before a job is declared dead.
102
+ *
103
+ * Three, with `JOB_BACKOFF_MS` below: a transient fault usually clears in
104
+ * minutes, and a fourth attempt an hour later is far more likely to be the
105
+ * same permanent fault again than a different outcome.
106
+ */
107
+ export declare const JOB_MAX_ATTEMPTS = 3;
108
+ /**
109
+ * Delay before attempt 2 and attempt 3.
110
+ *
111
+ * 🛑 Indexed by `attempt - 1`, so the array length must be at least
112
+ * `JOB_MAX_ATTEMPTS - 1`. The last value is reused if it is shorter, rather
113
+ * than producing `undefined` — a `nextAttemptAt` of `Invalid Date` would make
114
+ * the sweep's `<= now` comparison false forever and the retry would never run,
115
+ * which is the silent-stall shape this whole phase exists to remove.
116
+ */
117
+ export declare const JOB_BACKOFF_MS: readonly [number, number];
56
118
  export declare const JOB_FIELDS: {
57
119
  readonly JOB_TYPE: "jobType";
58
120
  readonly STATUS: "status";
@@ -63,10 +125,28 @@ export declare const JOB_FIELDS: {
63
125
  readonly FINISHED_AT: "finishedAt";
64
126
  readonly CREATED_AT: "createdAt";
65
127
  readonly UPDATED_AT: "updatedAt";
128
+ readonly ATTEMPT: "attempt";
129
+ readonly MAX_ATTEMPTS: "maxAttempts";
130
+ readonly NEXT_ATTEMPT_AT: "nextAttemptAt";
131
+ readonly PARENT_JOB_ID: "parentJobId";
66
132
  readonly STATUS_VALUES: {
67
133
  readonly PENDING: "pending";
68
134
  readonly PROCESSING: "processing";
69
135
  readonly DONE: "done";
70
136
  readonly FAILED: "failed";
137
+ /**
138
+ * Phase 8 — attempts exhausted. The dead-letter state.
139
+ *
140
+ * 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
141
+ * made a failed job terminal with no signal. `failed` now means "this
142
+ * attempt failed, and a retry is scheduled or the failure was permanent";
143
+ * `dead` means "we have stopped trying" and is the only one that opens a
144
+ * case.
145
+ *
146
+ * A reader that treats every non-`done` as hopeless loses that distinction —
147
+ * which is why the retention sweep excludes `dead` while an open case
148
+ * references it: the row is the evidence.
149
+ */
150
+ readonly DEAD: "dead";
71
151
  };
72
152
  };
@@ -21,7 +21,39 @@ export const JobStatusValues = {
21
21
  PROCESSING: "processing",
22
22
  DONE: "done",
23
23
  FAILED: "failed",
24
+ /**
25
+ * Phase 8 — attempts exhausted. The dead-letter state.
26
+ *
27
+ * 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
28
+ * made a failed job terminal with no signal. `failed` now means "this
29
+ * attempt failed, and a retry is scheduled or the failure was permanent";
30
+ * `dead` means "we have stopped trying" and is the only one that opens a
31
+ * case.
32
+ *
33
+ * A reader that treats every non-`done` as hopeless loses that distinction —
34
+ * which is why the retention sweep excludes `dead` while an open case
35
+ * references it: the row is the evidence.
36
+ */
37
+ DEAD: "dead",
24
38
  };
39
+ /**
40
+ * Attempts before a job is declared dead.
41
+ *
42
+ * Three, with `JOB_BACKOFF_MS` below: a transient fault usually clears in
43
+ * minutes, and a fourth attempt an hour later is far more likely to be the
44
+ * same permanent fault again than a different outcome.
45
+ */
46
+ export const JOB_MAX_ATTEMPTS = 3;
47
+ /**
48
+ * Delay before attempt 2 and attempt 3.
49
+ *
50
+ * 🛑 Indexed by `attempt - 1`, so the array length must be at least
51
+ * `JOB_MAX_ATTEMPTS - 1`. The last value is reused if it is shorter, rather
52
+ * than producing `undefined` — a `nextAttemptAt` of `Invalid Date` would make
53
+ * the sweep's `<= now` comparison false forever and the retry would never run,
54
+ * which is the silent-stall shape this whole phase exists to remove.
55
+ */
56
+ export const JOB_BACKOFF_MS = [5 * 60 * 1000, 30 * 60 * 1000];
25
57
  export const JOB_FIELDS = {
26
58
  JOB_TYPE: "jobType",
27
59
  STATUS: "status",
@@ -32,5 +64,10 @@ export const JOB_FIELDS = {
32
64
  FINISHED_AT: "finishedAt",
33
65
  CREATED_AT: "createdAt",
34
66
  UPDATED_AT: "updatedAt",
67
+ // Phase 8 retry/DLQ. `NEXT_ATTEMPT_AT` is the sweep's query field.
68
+ ATTEMPT: "attempt",
69
+ MAX_ATTEMPTS: "maxAttempts",
70
+ NEXT_ATTEMPT_AT: "nextAttemptAt",
71
+ PARENT_JOB_ID: "parentJobId",
35
72
  STATUS_VALUES: JobStatusValues,
36
73
  };
@@ -42,12 +42,12 @@ export declare const reopenLotterySlotSchema: z.ZodObject<{
42
42
  adminUserId: z.ZodString;
43
43
  }, "strip", z.ZodTypeAny, {
44
44
  slotNumber: number;
45
- sourceType: "event" | "product";
45
+ sourceType: "product" | "event";
46
46
  sourceId: string;
47
47
  adminUserId: string;
48
48
  }, {
49
49
  slotNumber: number;
50
- sourceType: "event" | "product";
50
+ sourceType: "product" | "event";
51
51
  sourceId: string;
52
52
  adminUserId: string;
53
53
  }>;
@@ -68,7 +68,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
68
68
  }, "strip", z.ZodTypeAny, {
69
69
  userId: string;
70
70
  transactionId: string;
71
- sourceType: "event" | "product";
71
+ sourceType: "product" | "event";
72
72
  paymentTime: string;
73
73
  purchasedItemNumber: number;
74
74
  userPhone: string;
@@ -79,7 +79,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
79
79
  }, {
80
80
  userId: string;
81
81
  transactionId: string;
82
- sourceType: "event" | "product";
82
+ sourceType: "product" | "event";
83
83
  paymentTime: string;
84
84
  purchasedItemNumber: number;
85
85
  userPhone: string;
@@ -90,7 +90,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
90
90
  }>, {
91
91
  userId: string;
92
92
  transactionId: string;
93
- sourceType: "event" | "product";
93
+ sourceType: "product" | "event";
94
94
  paymentTime: string;
95
95
  purchasedItemNumber: number;
96
96
  userPhone: string;
@@ -101,7 +101,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
101
101
  }, {
102
102
  userId: string;
103
103
  transactionId: string;
104
- sourceType: "event" | "product";
104
+ sourceType: "product" | "event";
105
105
  paymentTime: string;
106
106
  purchasedItemNumber: number;
107
107
  userPhone: string;