@mohasinac/appkit 4.60.0 → 4.61.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 (122) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/auctions/actions.d.ts +1 -1
  3. package/dist/_internal/server/features/auctions/actions.js +12 -1
  4. package/dist/_internal/server/features/blog/actions.d.ts +1 -1
  5. package/dist/_internal/server/features/blog/actions.js +12 -1
  6. package/dist/_internal/server/features/brands/actions.d.ts +1 -1
  7. package/dist/_internal/server/features/brands/actions.js +12 -1
  8. package/dist/_internal/server/features/cart/actions.d.ts +1 -1
  9. package/dist/_internal/server/features/cart/actions.js +12 -1
  10. package/dist/_internal/server/features/checkout/actions.js +0 -1
  11. package/dist/_internal/server/features/events/actions.d.ts +1 -1
  12. package/dist/_internal/server/features/events/actions.js +12 -1
  13. package/dist/_internal/server/features/gateway-events/redact.js +14 -2
  14. package/dist/_internal/server/features/history/actions.d.ts +1 -1
  15. package/dist/_internal/server/features/history/actions.js +12 -1
  16. package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
  17. package/dist/_internal/server/features/lottery/actions.js +12 -1
  18. package/dist/_internal/server/features/orders/actions.d.ts +1 -1
  19. package/dist/_internal/server/features/orders/actions.js +12 -1
  20. package/dist/_internal/server/features/orders/adapters.d.ts +20 -2
  21. package/dist/_internal/server/features/orders/adapters.js +18 -0
  22. package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
  23. package/dist/_internal/server/features/orders/service.js +31 -1
  24. package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
  25. package/dist/_internal/server/features/payouts/actions.js +12 -1
  26. package/dist/_internal/server/features/points/award.js +9 -1
  27. package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
  28. package/dist/_internal/server/features/pre-orders/actions.js +12 -1
  29. package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
  30. package/dist/_internal/server/features/prize-draws/actions.js +12 -1
  31. package/dist/_internal/server/features/products/actions.d.ts +1 -1
  32. package/dist/_internal/server/features/products/actions.js +12 -1
  33. package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
  34. package/dist/_internal/server/features/promotions/actions.js +12 -1
  35. package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
  36. package/dist/_internal/server/features/protection-claims/actions.js +21 -12
  37. package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
  38. package/dist/_internal/server/features/raffle/actions.js +12 -1
  39. package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
  40. package/dist/_internal/server/features/refunds/actions.js +14 -2
  41. package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
  42. package/dist/_internal/server/features/reviews/actions.js +12 -1
  43. package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
  44. package/dist/_internal/server/features/wishlist/actions.js +12 -1
  45. package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
  46. package/dist/_internal/server/jobs/core/paymentReconcile.js +84 -57
  47. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  48. package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
  49. package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
  50. package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
  51. package/dist/_internal/shared/features/promotions/schema.js +7 -0
  52. package/dist/_internal/shared/obligations/deadlines.js +11 -1
  53. package/dist/_internal/shared/tester/window.d.ts +25 -0
  54. package/dist/_internal/shared/tester/window.js +66 -0
  55. package/dist/client.d.ts +1 -0
  56. package/dist/client.js +3 -0
  57. package/dist/core/unit-of-work.js +15 -1
  58. package/dist/errors/firestore-codes.d.ts +37 -0
  59. package/dist/errors/firestore-codes.js +52 -0
  60. package/dist/errors/index.d.ts +0 -1
  61. package/dist/errors/index.js +12 -2
  62. package/dist/features/admin/components/AdminCouponEditorView.js +8 -0
  63. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
  64. package/dist/features/admin/components/DataListingView.js +2 -0
  65. package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
  66. package/dist/features/admin/repository/site-settings.repository.d.ts +2 -1
  67. package/dist/features/admin/repository/site-settings.repository.js +7 -0
  68. package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
  69. package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
  70. package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
  71. package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
  72. package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
  73. package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
  74. package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
  75. package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
  76. package/dist/features/categories/repository/price-observation.repository.js +21 -2
  77. package/dist/features/events/actions/event-actions.js +4 -1
  78. package/dist/features/events/components/AdminEventEditorView.js +72 -45
  79. package/dist/features/events/components/EventsListPageView.js +2 -1
  80. package/dist/features/events/schemas/firestore.d.ts +17 -0
  81. package/dist/features/events/schemas/firestore.js +1 -0
  82. package/dist/features/events/utils/sort-pinned.d.ts +33 -0
  83. package/dist/features/events/utils/sort-pinned.js +46 -0
  84. package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
  85. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  86. package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
  87. package/dist/features/inventory/components/ReceiveStationView.js +61 -17
  88. package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
  89. package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
  90. package/dist/features/obligations/repository/obligations.repository.js +10 -1
  91. package/dist/features/orders/schemas/firestore.d.ts +19 -0
  92. package/dist/features/orders/schemas/firestore.js +19 -0
  93. package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
  94. package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
  95. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  96. package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
  97. package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
  98. package/dist/features/products/components/ProductForm.js +2 -2
  99. package/dist/features/products/components/ProductGrid.js +1 -1
  100. package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
  101. package/dist/features/promotions/schemas/firestore.d.ts +22 -0
  102. package/dist/features/promotions/schemas/firestore.js +22 -0
  103. package/dist/features/promotions/schemas/index.d.ts +479 -6
  104. package/dist/features/promotions/schemas/index.js +153 -0
  105. package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
  106. package/dist/features/seller/schemas/coupon-form.d.ts +5 -0
  107. package/dist/features/seller/schemas/coupon-form.js +18 -0
  108. package/dist/features/shell/SectionForm.js +31 -9
  109. package/dist/features/sticker-trace/server.d.ts +2 -0
  110. package/dist/http/api-handler.js +4 -1
  111. package/dist/index.d.ts +3 -3
  112. package/dist/index.js +3 -3
  113. package/dist/next/routing/route-map.d.ts +0 -2
  114. package/dist/next/routing/route-map.js +10 -1
  115. package/dist/schemas/registry.d.ts +11 -6
  116. package/dist/server.js +7 -0
  117. package/dist/styles.css +51 -0
  118. package/dist/ui/components/PaginatedSelect.js +0 -0
  119. package/package.json +1 -1
  120. package/scripts/seed-cli.mjs +1822 -1812
  121. package/dist/features/tester/utils/phases.d.ts +0 -48
  122. package/dist/features/tester/utils/phases.js +0 -139
@@ -18,9 +18,32 @@ export const discountConfigSchema = z.object({
18
18
  maxDiscount: rupeesSchema.optional(),
19
19
  minPurchase: rupeesSchema.optional(),
20
20
  });
21
+ /*
22
+ * 🛑 THIS IS THE ONLY bxgy VALIDATOR. The four admin write paths — the POST
23
+ * and PATCH routes, both server actions, and `createCouponSchema` below — all
24
+ * import it rather than restating the shape.
25
+ *
26
+ * That is not tidiness. Until 2026-10-22 none of those four declared `bxgy` at
27
+ * all, and every one of them is a `z.object()`, which STRIPS unknown keys. So
28
+ * `AdminCouponEditorView` built a correct `bxgy` payload, the route answered
29
+ * 200, and the field was never written — an admin could create a BOGO coupon
30
+ * that discounted nothing, which is the exact silent-strip trap the `tiers`
31
+ * comment in the POST route already describes, recurring one field over.
32
+ *
33
+ * `maxItemPrice` is deliberately `.optional()` with no default: absent means
34
+ * "no ceiling", which is what every BOGO coupon written before it existed
35
+ * means. A default would retroactively shrink them.
36
+ */
21
37
  export const bxgyConfigSchema = z.object({
22
38
  buyQuantity: z.number().int().positive(),
23
39
  getQuantity: z.number().int().positive(),
40
+ // `.refine` rather than `.positive()` — `rupeesSchema` is a ZodEffects (it
41
+ // carries the 2-decimal check), and ZodNumber's numeric helpers are not on
42
+ // it. A zero ceiling would disqualify every unit, so it is rejected here as
43
+ // well as being treated as absent by `calculateBxgyDiscount`.
44
+ maxItemPrice: rupeesSchema
45
+ .refine((n) => n > 0, { message: "Price ceiling must be greater than zero" })
46
+ .optional(),
24
47
  applicableProducts: z.array(z.string()).optional(),
25
48
  applicableCategories: z.array(z.string()).optional(),
26
49
  });
@@ -70,6 +93,136 @@ export const couponFirestoreSchema = z.object({
70
93
  stats: couponStatsSchema,
71
94
  ...auditTimestampsShape,
72
95
  });
96
+ /**
97
+ * The admin create-coupon REQUEST BODY.
98
+ *
99
+ * 🛑 It lives here, in appkit, because `bxgyConfigSchema` does — and a zod
100
+ * schema CANNOT cross the version boundary. The consumer root is on zod 4 and
101
+ * appkit depends on zod 3, so `/api/admin/coupons` composing
102
+ * `bxgy: bxgyConfigSchema.optional()` into a consumer `z.object({...})` did
103
+ * not merely infer `unknown`: zod 4 **throws** on it —
104
+ *
105
+ * Invalid element at key "bx": expected a Zod schema
106
+ *
107
+ * — so every POST to that route failed. The type error was the only signal,
108
+ * and it reads as a cosmetic inference problem.
109
+ *
110
+ * Composed from the pieces above rather than restated, which is the concern
111
+ * the route's own comment raised about `bxgy`: "four write paths validate this
112
+ * shape and a fifth copy is how they drift". Re-declaring the shape in
113
+ * consumer zod would have been that fifth copy; moving the whole body schema
114
+ * to where the pieces live is the version of that fix which also works.
115
+ */
116
+ export const couponCreateBodySchema = z
117
+ .object({
118
+ code: z.string().min(2).max(50),
119
+ name: z.string().min(1),
120
+ description: z.string().default(""),
121
+ type: couponTypeEnumSchema,
122
+ tiers: z.array(tieredDiscountSchema).max(6).optional(),
123
+ bxgy: bxgyConfigSchema.optional(),
124
+ discount: z.object({
125
+ value: z.number().min(0),
126
+ maxDiscount: z.number().optional(),
127
+ minPurchase: z.number().optional(),
128
+ }),
129
+ usage: z.object({
130
+ totalLimit: z.number().optional(),
131
+ perUserLimit: z.number().optional(),
132
+ currentUsage: z.number().default(0),
133
+ }),
134
+ validity: z.object({
135
+ startDate: z.string().transform((v) => new Date(v)),
136
+ endDate: z
137
+ .string()
138
+ .optional()
139
+ .transform((v) => (v ? new Date(v) : undefined)),
140
+ isActive: z.boolean().default(false),
141
+ }),
142
+ restrictions: z
143
+ .object({
144
+ applicableProducts: z.array(z.string()).optional(),
145
+ applicableCategories: z.array(z.string()).optional(),
146
+ applicableSellers: z.array(z.string()).optional(),
147
+ excludeProducts: z.array(z.string()).optional(),
148
+ excludeCategories: z.array(z.string()).optional(),
149
+ firstTimeUserOnly: z.boolean().default(false),
150
+ })
151
+ .default({ firstTimeUserOnly: false }),
152
+ createdBy: z.string().optional().default("admin"),
153
+ })
154
+ .superRefine((data, ctx) => {
155
+ if (data.type === "percentage" && data.discount.value > 100) {
156
+ ctx.addIssue({
157
+ code: z.ZodIssueCode.custom,
158
+ message: "Percentage discount cannot exceed 100%",
159
+ path: ["discount", "value"],
160
+ });
161
+ }
162
+ });
163
+ /**
164
+ * The admin update-coupon REQUEST BODY (`PATCH /api/admin/coupons/[id]`).
165
+ *
166
+ * 🛑 HERE FOR THE SAME REASON AS `couponCreateBodySchema` — read its note
167
+ * first. It embeds `bxgyConfigSchema`, and a zod-3 schema composed into a
168
+ * consumer zod-4 `z.object({...})` THROWS at parse time rather than merely
169
+ * mistyping. The POST route was migrated when that was discovered; this is
170
+ * the sibling PATCH, which has the same embed and would fail the same way.
171
+ *
172
+ * 🛑 NOT `couponCreateBodySchema.partial()`. That schema carries a
173
+ * `.superRefine`, so it is a ZodEffects and has no `.partial()` — and the
174
+ * update body is genuinely a different shape anyway: no `code` (a coupon's
175
+ * code is immutable after creation), plus the `action` discriminator the
176
+ * route uses for activate/deactivate.
177
+ *
178
+ * Every nested object stays OPTIONAL as a whole, which is what lets the route
179
+ * merge `validity` and `restrictions` against the stored document instead of
180
+ * replacing them — a caller sending only `{isActive: false}` must not wipe
181
+ * the coupon's real dates.
182
+ */
183
+ export const couponUpdateBodySchema = z.object({
184
+ name: z.string().min(1).optional(),
185
+ description: z.string().optional(),
186
+ tiers: z.array(tieredDiscountSchema).max(6).optional(),
187
+ bxgy: bxgyConfigSchema.optional(),
188
+ discount: z
189
+ .object({
190
+ value: z.number().min(0),
191
+ maxDiscount: z.number().optional(),
192
+ minPurchase: z.number().optional(),
193
+ })
194
+ .optional(),
195
+ usage: z
196
+ .object({
197
+ totalLimit: z.number().optional(),
198
+ perUserLimit: z.number().optional(),
199
+ })
200
+ .optional(),
201
+ validity: z
202
+ .object({
203
+ startDate: z
204
+ .string()
205
+ .transform((v) => new Date(v))
206
+ .optional(),
207
+ endDate: z
208
+ .string()
209
+ .optional()
210
+ .transform((v) => (v ? new Date(v) : undefined)),
211
+ isActive: z.boolean().optional(),
212
+ })
213
+ .optional(),
214
+ restrictions: z
215
+ .object({
216
+ applicableProducts: z.array(z.string()).optional(),
217
+ applicableCategories: z.array(z.string()).optional(),
218
+ applicableSellers: z.array(z.string()).optional(),
219
+ excludeProducts: z.array(z.string()).optional(),
220
+ excludeCategories: z.array(z.string()).optional(),
221
+ firstTimeUserOnly: z.boolean().optional(),
222
+ })
223
+ .optional(),
224
+ action: z.enum(["activate", "deactivate"]).optional(),
225
+ });
73
226
  export const couponUsageFirestoreSchema = z.object({
74
227
  id: z.string(),
75
228
  userId: z.string(),
@@ -1,20 +1,11 @@
1
+ // One shared predicate — this file had its own copy; see firestore-codes.ts.
2
+ import { isAlreadyExistsError } from "../../../errors/firestore-codes.js";
1
3
  import { BaseRepository, prepareForFirestore } from "../../../providers/db-firebase/index.js";
2
4
  import { normalizeError } from "../../../errors/normalize.js";
3
5
  import { buildHistoryEntry } from "../../../_internal/shared/history/history.js";
4
6
  import { ConflictError } from "../../../errors/index.js";
5
7
  import { PROTECTION_CLAIMS_COLLECTION, PROTECTION_CLAIM_ACTIVE_STATUSES, PROTECTION_CLAIM_FIELDS, createProtectionClaimId, } from "../schemas/firestore.js";
6
- const GRPC_ALREADY_EXISTS = 6;
7
- function isAlreadyExists(err) {
8
- if (typeof err !== "object" || err === null)
9
- return false;
10
- const code = err.code;
11
- if (code === GRPC_ALREADY_EXISTS)
12
- return true;
13
- if (code === "already-exists" || code === "ALREADY_EXISTS")
14
- return true;
15
- const message = err.message;
16
- return typeof message === "string" && message.includes("ALREADY_EXISTS");
17
- }
8
+ import { serverLogger } from "../../../monitoring/index.js";
18
9
  export class ProtectionClaimsRepository extends BaseRepository {
19
10
  constructor() {
20
11
  super(PROTECTION_CLAIMS_COLLECTION);
@@ -64,7 +55,7 @@ export class ProtectionClaimsRepository extends BaseRepository {
64
55
  return doc;
65
56
  }
66
57
  catch (err) {
67
- if (isAlreadyExists(err)) {
58
+ if (isAlreadyExistsError(err)) {
68
59
  void normalizeError(err);
69
60
  throw new ConflictError(`A ${input.kind} protection claim is already open on this order. Add to that claim rather than filing a second one.`);
70
61
  }
@@ -118,7 +109,15 @@ export class ProtectionClaimsRepository extends BaseRepository {
118
109
  await this.getCollection().doc(id).update({ refundId, updatedAt: now });
119
110
  }
120
111
  catch (err) {
121
- void normalizeError(err);
112
+ /*
113
+ * Best effort, but RECORDED. `void normalizeError(err)` alone types the
114
+ * error and reports it nowhere — as silent as `catch {}` while looking
115
+ * handled, which is what audit-silent-degrade calls normalize theatre.
116
+ * Swallowing is still correct here (the claim row is already durable; this is a best-effort annotation); losing the reason is not.
117
+ */
118
+ serverLogger.warn("protectionClaims.write failed", {
119
+ error: normalizeError(err).message,
120
+ });
122
121
  }
123
122
  }
124
123
  async setStatus(id, status, reason, now = new Date()) {
@@ -142,7 +141,15 @@ export class ProtectionClaimsRepository extends BaseRepository {
142
141
  });
143
142
  }
144
143
  catch (err) {
145
- void normalizeError(err);
144
+ /*
145
+ * Best effort, but RECORDED. `void normalizeError(err)` alone types the
146
+ * error and reports it nowhere — as silent as `catch {}` while looking
147
+ * handled, which is what audit-silent-degrade calls normalize theatre.
148
+ * Swallowing is still correct here (the claim row is already durable; this is a best-effort annotation); losing the reason is not.
149
+ */
150
+ serverLogger.warn("protectionClaims.write failed", {
151
+ error: normalizeError(err).message,
152
+ });
146
153
  }
147
154
  }
148
155
  async findByOrderAndKind(orderId, kind) {
@@ -199,6 +199,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
199
199
  description: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
200
200
  buyQuantity: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
201
201
  getQuantity: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
202
+ maxItemPrice: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
202
203
  currentUsage: z.ZodOptional<z.ZodNumber>;
203
204
  tiers: z.ZodOptional<z.ZodArray<z.ZodObject<{
204
205
  minAmount: z.ZodNumber;
@@ -233,6 +234,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
233
234
  discountValue: number;
234
235
  }[] | undefined;
235
236
  currentUsage?: number | undefined;
237
+ maxItemPrice?: string | undefined;
236
238
  maxDiscount?: string | undefined;
237
239
  buyQuantity?: string | undefined;
238
240
  getQuantity?: string | undefined;
@@ -257,6 +259,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
257
259
  discountValue: number;
258
260
  }[] | undefined;
259
261
  currentUsage?: number | undefined;
262
+ maxItemPrice?: string | undefined;
260
263
  maxDiscount?: string | undefined;
261
264
  buyQuantity?: string | undefined;
262
265
  getQuantity?: string | undefined;
@@ -281,6 +284,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
281
284
  discountValue: number;
282
285
  }[] | undefined;
283
286
  currentUsage?: number | undefined;
287
+ maxItemPrice?: string | undefined;
284
288
  maxDiscount?: string | undefined;
285
289
  buyQuantity?: string | undefined;
286
290
  getQuantity?: string | undefined;
@@ -305,6 +309,7 @@ export declare const adminCouponFormSchema: z.ZodEffects<z.ZodObject<{
305
309
  discountValue: number;
306
310
  }[] | undefined;
307
311
  currentUsage?: number | undefined;
312
+ maxItemPrice?: string | undefined;
308
313
  maxDiscount?: string | undefined;
309
314
  buyQuantity?: string | undefined;
310
315
  getQuantity?: string | undefined;
@@ -236,6 +236,24 @@ export const adminCouponFormSchema = couponFormBase
236
236
  section: "basics", order: 7, row: "pair", kind: "number",
237
237
  when: (v) => v.type === "buy_x_get_y",
238
238
  }),
239
+ /*
240
+ * The per-ITEM price ceiling — "buy 3 get 1 free on items under ₹2,000".
241
+ *
242
+ * 🛑 Genuinely optional, unlike the buy/get pair: `superRefine` below does
243
+ * NOT demand it when the type is buy_x_get_y, because a BOGO with no
244
+ * ceiling is a perfectly ordinary offer and every one written before
245
+ * 2026-10-22 is exactly that. Leaving it blank means no ceiling.
246
+ *
247
+ * 🛑 Not to be confused with `minPurchase`, which is the ORDER total. This
248
+ * is one unit's price, so a line of three ₹1,500 tops qualifies on all
249
+ * three units rather than failing a ₹4,500 test.
250
+ */
251
+ maxItemPrice: annotate(numericString("Maximum item price").optional(), {
252
+ section: "basics", order: 8, row: "pair", kind: "number",
253
+ when: (v) => v.type === "buy_x_get_y",
254
+ label: "Max item price (₹)",
255
+ help: "Only items at or below this price qualify. Leave blank for no limit.",
256
+ }),
239
257
  /*
240
258
  * Written by orders, never by this form. `t3-mirror` renders it read-only
241
259
  * instead of as a box an admin could set to whatever they liked — which is
@@ -391,6 +391,21 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
391
391
  * sections").
392
392
  */
393
393
  const showRail = ordered.length >= 3;
394
+ /**
395
+ * One rail click: open the section if it is not already, then scroll to it.
396
+ * Named rather than inline in the button's `onClick`, where the `if` body
397
+ * sat a brace too deep inside the mapped JSX.
398
+ *
399
+ * Opening is conditional on purpose — in `single` mode an unconditional
400
+ * `setOpenIds([id])` would COLLAPSE the section you are already in when you
401
+ * click its own rail entry, so the rail would look like it closed the form.
402
+ */
403
+ const openAndScrollTo = (id) => {
404
+ if (!openIds.includes(id)) {
405
+ setOpenIds(expandMode === "single" ? [id] : [...openIds, id]);
406
+ }
407
+ scrollToSection(id);
408
+ };
394
409
  /*
395
410
  * `sticky` under the measured header, and it publishes NO height: per the
396
411
  * three-tier rule a sticky element CONSUMES the tier and never joins it.
@@ -408,21 +423,28 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
408
423
  * comment syntax does not parse. That is the fourth time today; the rule is
409
424
  * simply that JSX comments belong in CHILDREN position and nowhere else.
410
425
  */
411
- const rail = showRail ? (_jsx(Div, { overflow: "auto", className: "hidden lg:block w-56 flex-shrink-0 sticky self-start top-[calc(var(--header-height,3.5rem)+1rem)] max-h-[calc(100vh-var(--header-height,3.5rem)-2rem)]", children: _jsx(Nav, { "aria-label": "Form sections", children: _jsx(Stack, { gap: "px", children: ordered.map((section) => {
426
+ const rail = showRail ? (_jsx(Div, { overflow: "auto",
427
+ /*
428
+ * <StickyToolbar> is a horizontal translucent bar: it ships `border-b`, a
429
+ * z-10 layer and the dismiss/re-expand CollapseStrip, and offers no
430
+ * `self-start`, `max-h` or overflow scrolling. Using it here would paint
431
+ * a bottom border under a 14rem sidebar and give it a horizontal bar's
432
+ * collapse affordance.
433
+ *
434
+ * 🛑 The marker below must stay on the line DIRECTLY above the className.
435
+ * Every one of these audits reads `lines[i - 1]` and nothing further, so
436
+ * a tidy explanation with the marker on its first line suppresses
437
+ * nothing while looking suppressed to a human (CLAUDE.md § Rule #5).
438
+ */
439
+ // audit-raw-sticky-toolbar-ok: a vertical RAIL, not a toolbar — see above.
440
+ className: "hidden lg:block w-56 flex-shrink-0 sticky self-start top-[calc(var(--header-height,3.5rem)+1rem)] max-h-[calc(100vh-var(--header-height,3.5rem)-2rem)]", children: _jsx(Nav, { "aria-label": "Form sections", children: _jsx(Stack, { gap: "px", children: ordered.map((section) => {
412
441
  const errorCount = errorCountFor(section);
413
442
  const isOpen = section.required === true ||
414
443
  openIds.includes(section.id) ||
415
444
  forceOpenIds.includes(section.id);
416
445
  return (_jsxs(Button, { type: "button",
417
446
  /* `ghost`, not `bare` — UI_BUTTON has no `bare` variant. */
418
- variant: "ghost", size: "sm", justify: "between", gap: "sm", className: `w-full text-left ${isOpen ? "appkit-section-rail__item--active" : ""}`, "aria-current": isOpen ? "true" : undefined, onClick: () => {
419
- if (!openIds.includes(section.id)) {
420
- setOpenIds(expandMode === "single"
421
- ? [section.id]
422
- : [...openIds, section.id]);
423
- }
424
- scrollToSection(section.id);
425
- }, children: [_jsx(Span, { size: "sm", color: isOpen ? "primary" : "muted", children: section.label }), errorCount > 0 && (_jsx(Span, { size: "xs", color: "error", weight: "semibold", children: errorCount }))] }, section.id));
447
+ variant: "ghost", size: "sm", justify: "between", gap: "sm", className: `w-full text-left ${isOpen ? "appkit-section-rail__item--active" : ""}`, "aria-current": isOpen ? "true" : undefined, onClick: () => openAndScrollTo(section.id), children: [_jsx(Span, { size: "sm", color: isOpen ? "primary" : "muted", children: section.label }), errorCount > 0 && (_jsx(Span, { size: "xs", color: "error", weight: "semibold", children: errorCount }))] }, section.id));
426
448
  }) }) }) })) : null;
427
449
  const panels = (_jsxs(Stack, { gap: "md", children: [ordered.map((section) => {
428
450
  const isRequired = section.required === true;
@@ -6,3 +6,5 @@ export { rasterisePartToBox } from "../../_internal/shared/features/sticker-trac
6
6
  export { buildCutFileBytes, buildCutFileForRun } from "../../_internal/server/features/sticker-trace/build-cut-file";
7
7
  export { collectMosaicInputs, type MosaicInputs, type MosaicPartInput, } from "../../_internal/server/features/sticker-trace/mosaic-inputs";
8
8
  export type { PrintPageSizeId } from "../../_internal/shared/features/sticker-trace/export/page-sizes";
9
+ export type { DecodeOptions, ImageCodec, PreviewOptions, ProbeResult, } from "../../_internal/shared/features/sticker-trace/codec";
10
+ export type { DecodedRaster, EncodedImage, Rect as StickerRect, } from "../../_internal/shared/features/sticker-trace/types";
@@ -2,7 +2,10 @@ import { normalizeError } from "../errors/normalize.js";
2
2
  import { isAccountDisabled } from "../features/auth/role-predicates.js";
3
3
  import { createApiHandlerFactory } from "../next/api/apiHandler.js";
4
4
  import { applyRateLimit } from "../security/index.js";
5
- import { AuthenticationError, AuthorizationError, ERROR_MESSAGES, handleApiError, } from "../errors/index.js";
5
+ import { AuthenticationError, AuthorizationError, ERROR_MESSAGES, } from "../errors/index.js";
6
+ // Next-coupled, so it comes from the defining module — see errors/index.ts.
7
+ // This file is an API-route handler, so `next/server` here is correct.
8
+ import { handleApiError } from "../errors/error-handler.js";
6
9
  import { errorResponse } from "../next/api/api-response.js";
7
10
  import { serverLogger } from "../monitoring/index.js";
8
11
  import { getProviders } from "../contracts/index.js";
package/dist/index.d.ts CHANGED
@@ -321,9 +321,9 @@ export { ERROR_CODES, BID_ERROR_CODES, OFFER_ERROR_CODES } from "./errors/index"
321
321
  export { ERROR_MESSAGES } from "./errors/index";
322
322
  export { NotFoundError } from "./errors/index";
323
323
  export { ValidationError } from "./errors/index";
324
- export { handleApiError } from "./errors/index";
325
- export { isAppError } from "./errors/index";
326
- export { logError } from "./errors/index";
324
+ export { handleApiError } from "./errors/error-handler";
325
+ export { isAppError } from "./errors/error-handler";
326
+ export { logError } from "./errors/error-handler";
327
327
  export type { ErrorCode } from "./errors/index";
328
328
  export { PaymentProviderUnreachableError } from "./errors/index";
329
329
  export { mapToHttpError, HTTP_ERROR_CODES } from "./errors/index";
package/dist/index.js CHANGED
@@ -552,13 +552,13 @@ export { NotFoundError } from "./errors/index.js";
552
552
  export { ValidationError } from "./errors/index.js";
553
553
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
554
554
  // handleApiError - Model for handle api error.
555
- export { handleApiError } from "./errors/index.js";
555
+ export { handleApiError } from "./errors/error-handler.js";
556
556
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
557
557
  // isAppError - Model for is app error.
558
- export { isAppError } from "./errors/index.js";
558
+ export { isAppError } from "./errors/error-handler.js";
559
559
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
560
560
  // logError - Helper for log error.
561
- export { logError } from "./errors/index.js";
561
+ export { logError } from "./errors/error-handler.js";
562
562
  // [UTIL]-Pure utility — no framework or runtime dependency; safe to import from any environment.
563
563
  // PaymentProviderUnreachableError - Thrown when the payment gateway API exhausts retries.
564
564
  export { PaymentProviderUnreachableError } from "./errors/index.js";
@@ -299,7 +299,6 @@ export declare const DEFAULT_ROUTE_MAP: {
299
299
  readonly ADMIN: {
300
300
  readonly DASHBOARD: "/admin/dashboard";
301
301
  readonly SHIPMENTS: "/admin/shipments";
302
- readonly INVENTORY: "/admin/inventory";
303
302
  readonly INVENTORY_BOXES: "/admin/inventory/boxes";
304
303
  readonly INVENTORY_BOX_DETAIL: (id: string) => string;
305
304
  readonly INVENTORY_BOX_LABELS: "/admin/inventory/boxes/labels";
@@ -752,7 +751,6 @@ export declare const ROUTES: {
752
751
  readonly ADMIN: {
753
752
  readonly DASHBOARD: "/admin/dashboard";
754
753
  readonly SHIPMENTS: "/admin/shipments";
755
- readonly INVENTORY: "/admin/inventory";
756
754
  readonly INVENTORY_BOXES: "/admin/inventory/boxes";
757
755
  readonly INVENTORY_BOX_DETAIL: (id: string) => string;
758
756
  readonly INVENTORY_BOX_LABELS: "/admin/inventory/boxes/labels";
@@ -433,7 +433,16 @@ export const DEFAULT_ROUTE_MAP = {
433
433
  ADMIN: {
434
434
  DASHBOARD: "/admin/dashboard",
435
435
  SHIPMENTS: "/admin/shipments",
436
- INVENTORY: "/admin/inventory",
436
+ /*
437
+ * 🛑 There is deliberately NO `INVENTORY: "/admin/inventory"` key.
438
+ *
439
+ * I added one and never built the page behind it, and nothing ever
440
+ * referenced it — `audit-dead-route-key` caught it. A ROUTES key is a
441
+ * promise, and the two real surfaces (`INVENTORY_BOXES`,
442
+ * `INVENTORY_MOVEMENTS`) each carry their own nav entry, so an index page
443
+ * would be a third place to list the same two links. Build it only when
444
+ * there is a third child worth grouping.
445
+ */
437
446
  INVENTORY_BOXES: "/admin/inventory/boxes",
438
447
  INVENTORY_BOX_DETAIL: (id) => `/admin/inventory/boxes/${id}`,
439
448
  /*
@@ -1994,6 +1994,9 @@ export declare const SCHEMAS: {
1994
1994
  }, import("zod").ZodTypeAny, "passthrough">;
1995
1995
  userId: string;
1996
1996
  couponCode: string;
1997
+ claimedAt: string | Date | import("zod").objectOutputType<{
1998
+ toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
1999
+ }, import("zod").ZodTypeAny, "passthrough">;
1997
2000
  couponId: string;
1998
2001
  couponSnapshot: {
1999
2002
  type: "fixed" | "percentage" | "free_shipping" | "buy_x_get_y";
@@ -2015,9 +2018,6 @@ export declare const SCHEMAS: {
2015
2018
  storeId?: string | undefined;
2016
2019
  description?: string | undefined;
2017
2020
  };
2018
- claimedAt: string | Date | import("zod").objectOutputType<{
2019
- toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2020
- }, import("zod").ZodTypeAny, "passthrough">;
2021
2021
  expiresAt?: string | Date | import("zod").objectOutputType<{
2022
2022
  toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2023
2023
  }, import("zod").ZodTypeAny, "passthrough"> | null | undefined;
@@ -2034,6 +2034,9 @@ export declare const SCHEMAS: {
2034
2034
  }, import("zod").ZodTypeAny, "passthrough">;
2035
2035
  userId: string;
2036
2036
  couponCode: string;
2037
+ claimedAt: string | Date | import("zod").objectInputType<{
2038
+ toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2039
+ }, import("zod").ZodTypeAny, "passthrough">;
2037
2040
  couponId: string;
2038
2041
  couponSnapshot: {
2039
2042
  type: "fixed" | "percentage" | "free_shipping" | "buy_x_get_y";
@@ -2055,9 +2058,6 @@ export declare const SCHEMAS: {
2055
2058
  storeId?: string | undefined;
2056
2059
  description?: string | undefined;
2057
2060
  };
2058
- claimedAt: string | Date | import("zod").objectInputType<{
2059
- toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2060
- }, import("zod").ZodTypeAny, "passthrough">;
2061
2061
  expiresAt?: string | Date | import("zod").objectInputType<{
2062
2062
  toDate: import("zod").ZodFunction<import("zod").ZodTuple<[], import("zod").ZodUnknown>, import("zod").ZodDate>;
2063
2063
  }, import("zod").ZodTypeAny, "passthrough"> | null | undefined;
@@ -2105,16 +2105,19 @@ export declare const SCHEMAS: {
2105
2105
  bxgy: import("zod").ZodOptional<import("zod").ZodObject<{
2106
2106
  buyQuantity: import("zod").ZodNumber;
2107
2107
  getQuantity: import("zod").ZodNumber;
2108
+ maxItemPrice: import("zod").ZodOptional<import("zod").ZodEffects<import("zod").ZodEffects<import("zod").ZodNumber, number, number>, number, number>>;
2108
2109
  applicableProducts: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
2109
2110
  applicableCategories: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
2110
2111
  }, "strip", import("zod").ZodTypeAny, {
2111
2112
  buyQuantity: number;
2112
2113
  getQuantity: number;
2114
+ maxItemPrice?: number | undefined;
2113
2115
  applicableProducts?: string[] | undefined;
2114
2116
  applicableCategories?: string[] | undefined;
2115
2117
  }, {
2116
2118
  buyQuantity: number;
2117
2119
  getQuantity: number;
2120
+ maxItemPrice?: number | undefined;
2118
2121
  applicableProducts?: string[] | undefined;
2119
2122
  applicableCategories?: string[] | undefined;
2120
2123
  }>>;
@@ -2261,6 +2264,7 @@ export declare const SCHEMAS: {
2261
2264
  bxgy?: {
2262
2265
  buyQuantity: number;
2263
2266
  getQuantity: number;
2267
+ maxItemPrice?: number | undefined;
2264
2268
  applicableProducts?: string[] | undefined;
2265
2269
  applicableCategories?: string[] | undefined;
2266
2270
  } | undefined;
@@ -2319,6 +2323,7 @@ export declare const SCHEMAS: {
2319
2323
  bxgy?: {
2320
2324
  buyQuantity: number;
2321
2325
  getQuantity: number;
2326
+ maxItemPrice?: number | undefined;
2322
2327
  applicableProducts?: string[] | undefined;
2323
2328
  applicableCategories?: string[] | undefined;
2324
2329
  } | undefined;
package/dist/server.js CHANGED
@@ -1528,6 +1528,13 @@ export { SUBDIVISIONS, subdivisionsFor } from "./constants/geo/subdivisions.js";
1528
1528
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry.js";
1529
1529
  export { ActionIndexRepository, actionIndexRepository, } from "./features/search/action-index/repository.js";
1530
1530
  export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy.js";
1531
+ /*
1532
+ * 🛑 `wouldBeFullRefund` is deliberately NOT exported. I added it here, and
1533
+ * `audit-appkit-reexports` was right to refuse it: `_internal/shared/checkout/`
1534
+ * is not a `features/` module, so exporting it would have made a money
1535
+ * predicate public purely so one route could duplicate a decision
1536
+ * `processRefundAction` already makes — see the note on `ProcessRefundInput`.
1537
+ */
1531
1538
  // Return / refund reasons — the closed enum the final-sale gate branches on.
1532
1539
  // See the module docstring for why the exempt set is "seller or carrier fault"
1533
1540
  // rather than "serious reasons".
package/dist/styles.css CHANGED
@@ -11350,6 +11350,57 @@ body:has(.appkit-page-breadcrumb) .appkit-auto-breadcrumbs {
11350
11350
  * plain file like this one can never win an importance tie.
11351
11351
  */
11352
11352
 
11353
+ /* The kiss-cut label sheet. Shipped unbundled when it was written — the
11354
+ `.appkit-label-sheet` grid and its `@page` rules reached no consumer, so
11355
+ every printed sheet fell back to browser defaults and the die-cut
11356
+ alignment the component exists to produce could not happen. */
11357
+ /*
11358
+ * Print-layout classes for physical labels and printable documents.
11359
+ *
11360
+ * 🛑 THESE LIVE IN A STATIC STYLESHEET, NOT ONLY IN `labelPageCss`'s STRING.
11361
+ *
11362
+ * `labelPageCss()` and `documentPageCss()` emit an `@page` rule plus these
11363
+ * same `break-*` declarations into a runtime `<style>`. That is necessary for
11364
+ * `@page` — its `size` is computed from the chosen profile, so it cannot be
11365
+ * static — but it meant `.appkit-label` and `.appkit-label-sheet` existed
11366
+ * ONLY inside a template literal.
11367
+ *
11368
+ * Consequence: `audit-dead-css-classes` reported them as classes generating no
11369
+ * CSS rule, correctly, because nothing it can read defines them. A class whose
11370
+ * only definition is a string concatenation is invisible to every tool — the
11371
+ * audit, an editor's go-to-definition, and anyone grepping the stylesheets.
11372
+ * Declaring them here makes them real; the runtime string still carries its
11373
+ * own copy because a print view may be opened in a context where the bundled
11374
+ * stylesheet has not loaded, and a duplicated `break-inside: avoid` is
11375
+ * harmless where a missing one splits a label across two pages.
11376
+ *
11377
+ * @tag domain:print
11378
+ * @tag layer:feature
11379
+ */
11380
+
11381
+ @media print {
11382
+ /*
11383
+ * A label must never be split across a page boundary. On thermal stock a
11384
+ * page IS a label, so a split would print the top half on one label and the
11385
+ * bottom half on the next — and the operator discovers it after both have
11386
+ * been peeled and stuck to something.
11387
+ */
11388
+ .appkit-label {
11389
+ break-inside: avoid;
11390
+ page-break-inside: avoid;
11391
+ }
11392
+
11393
+ /*
11394
+ * A sheet is one A4 page of die-cut labels, or one contents sheet. Same
11395
+ * rule, different unit — splitting it would offset every label after the
11396
+ * break from its die-cut hole.
11397
+ */
11398
+ .appkit-label-sheet {
11399
+ break-inside: avoid;
11400
+ page-break-inside: avoid;
11401
+ }
11402
+ }
11403
+
11353
11404
  /*
11354
11405
  * Third-party stylesheet, vendored into dist/vendor by copy-assets.mjs.
11355
11406
  * It used to be a JS import inside ImageEditor.tsx, which made it invisible to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.60.0",
3
+ "version": "4.61.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"