@mohasinac/appkit 4.61.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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +177 -9
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +22 -4
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +60 -12
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `disputeReasons` — the staff-curated list a buyer picks from when raising a
|
|
3
|
+
* dispute.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is NOT
|
|
6
|
+
*
|
|
7
|
+
* 🛑 It is not policy. Every row carries a `kind`, and the GATE lives on the
|
|
8
|
+
* kind — see `_internal/shared/features/orders/dispute-reasons.ts`. A row can
|
|
9
|
+
* never introduce a new kind of claim, because a gate cannot branch on a
|
|
10
|
+
* user-created string.
|
|
11
|
+
*
|
|
12
|
+
* So this list is for TRIAGE and ANALYTICS: "what are buyers actually
|
|
13
|
+
* reporting", answered without reading free text. Staff (admin + employees)
|
|
14
|
+
* add rows as real gaps appear; buyers select only.
|
|
15
|
+
*
|
|
16
|
+
* ## Why staff-creatable rather than a closed enum
|
|
17
|
+
*
|
|
18
|
+
* Its sibling `RETURN_REASON` *is* a closed enum, and deliberately so: each
|
|
19
|
+
* member there decides whether a final sale can refuse the return, which is a
|
|
20
|
+
* money consequence that must be reviewed in code. Nothing here decides money.
|
|
21
|
+
* A support agent who keeps seeing "the courier left it at the wrong flat"
|
|
22
|
+
* should be able to add it without a release.
|
|
23
|
+
*/
|
|
24
|
+
import type { DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
|
|
25
|
+
export declare const DISPUTE_REASON_COLLECTION = "disputeReasons";
|
|
26
|
+
export interface DisputeReasonDocument {
|
|
27
|
+
/** `dreason-<slug of label>`, so the same reason cannot be added twice. */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Buyer-facing. Shown verbatim in the picker. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Which gate this reason is subject to. Never free-form. */
|
|
32
|
+
kind: DisputeKind;
|
|
33
|
+
/**
|
|
34
|
+
* 🛑 Soft-retire, never delete.
|
|
35
|
+
*
|
|
36
|
+
* A past dispute names this reason. Deleting the row would orphan that
|
|
37
|
+
* record and take the author with it — and the order snapshots the LABEL
|
|
38
|
+
* precisely so history survives either way, but a deleted row still loses
|
|
39
|
+
* "who added this, and when" for anyone auditing the list itself.
|
|
40
|
+
*/
|
|
41
|
+
isActive: boolean;
|
|
42
|
+
/** Display name, so a seller or agent can see whose wording this is. */
|
|
43
|
+
createdByName: string;
|
|
44
|
+
/** The uid, for moderation. Never rendered. */
|
|
45
|
+
createdByUid: string;
|
|
46
|
+
createdAt: Date;
|
|
47
|
+
updatedAt: Date;
|
|
48
|
+
}
|
|
49
|
+
export declare const DISPUTE_REASON_FIELDS: {
|
|
50
|
+
readonly ID: "id";
|
|
51
|
+
readonly LABEL: "label";
|
|
52
|
+
readonly KIND: "kind";
|
|
53
|
+
readonly IS_ACTIVE: "isActive";
|
|
54
|
+
readonly CREATED_BY_UID: "createdByUid";
|
|
55
|
+
readonly CREATED_AT: "createdAt";
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Bounds on a label.
|
|
59
|
+
*
|
|
60
|
+
* Short on purpose: this is a picker option, not the buyer's explanation. The
|
|
61
|
+
* detail goes in the free-text field on the dispute itself, which is where a
|
|
62
|
+
* buyer should describe what actually happened.
|
|
63
|
+
*/
|
|
64
|
+
export declare const DISPUTE_REASON_LABEL_MAX = 80;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `disputeReasons` — the staff-curated list a buyer picks from when raising a
|
|
3
|
+
* dispute.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is NOT
|
|
6
|
+
*
|
|
7
|
+
* 🛑 It is not policy. Every row carries a `kind`, and the GATE lives on the
|
|
8
|
+
* kind — see `_internal/shared/features/orders/dispute-reasons.ts`. A row can
|
|
9
|
+
* never introduce a new kind of claim, because a gate cannot branch on a
|
|
10
|
+
* user-created string.
|
|
11
|
+
*
|
|
12
|
+
* So this list is for TRIAGE and ANALYTICS: "what are buyers actually
|
|
13
|
+
* reporting", answered without reading free text. Staff (admin + employees)
|
|
14
|
+
* add rows as real gaps appear; buyers select only.
|
|
15
|
+
*
|
|
16
|
+
* ## Why staff-creatable rather than a closed enum
|
|
17
|
+
*
|
|
18
|
+
* Its sibling `RETURN_REASON` *is* a closed enum, and deliberately so: each
|
|
19
|
+
* member there decides whether a final sale can refuse the return, which is a
|
|
20
|
+
* money consequence that must be reviewed in code. Nothing here decides money.
|
|
21
|
+
* A support agent who keeps seeing "the courier left it at the wrong flat"
|
|
22
|
+
* should be able to add it without a release.
|
|
23
|
+
*/
|
|
24
|
+
export const DISPUTE_REASON_COLLECTION = "disputeReasons";
|
|
25
|
+
export const DISPUTE_REASON_FIELDS = {
|
|
26
|
+
ID: "id",
|
|
27
|
+
LABEL: "label",
|
|
28
|
+
KIND: "kind",
|
|
29
|
+
IS_ACTIVE: "isActive",
|
|
30
|
+
CREATED_BY_UID: "createdByUid",
|
|
31
|
+
CREATED_AT: "createdAt",
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Bounds on a label.
|
|
35
|
+
*
|
|
36
|
+
* Short on purpose: this is a picker option, not the buyer's explanation. The
|
|
37
|
+
* detail goes in the free-text field on the dispute itself, which is where a
|
|
38
|
+
* buyer should describe what actually happened.
|
|
39
|
+
*/
|
|
40
|
+
export const DISPUTE_REASON_LABEL_MAX = 80;
|
|
@@ -407,10 +407,10 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
407
407
|
// The per-step `validate` is gone: every rule it stated is already in
|
|
408
408
|
// eventDraftSchema (title/startsAt/endsAt are .min(1)), and it only ever
|
|
409
409
|
// acted as a gate to the next step.
|
|
410
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Event Details" }), !isEdit && (_jsx(Select, { label: "Event type", value: values.type, options: allowedTypes ? EVENT_TYPE_OPTIONS.filter((o) => allowedTypes.includes(o.value)) : EVENT_TYPE_OPTIONS, onChange: (e) => onChange({ type: e.target.value }) })), _jsx(Input, { label: "Title", value: values.title, onChange: (e) => {
|
|
410
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Event Details" }), !isEdit && (_jsx(Select, { label: "Event type", error: errors.type, value: values.type, options: allowedTypes ? EVENT_TYPE_OPTIONS.filter((o) => allowedTypes.includes(o.value)) : EVENT_TYPE_OPTIONS, onChange: (e) => onChange({ type: e.target.value }) })), _jsx(Input, { label: "Title", error: errors.title, value: values.title, onChange: (e) => {
|
|
411
411
|
const v = e.target.value;
|
|
412
412
|
onChange({ title: v, ...(!slugManual && { slug: toEventSlug(v) }) });
|
|
413
|
-
}, placeholder: "Charizard Flash Sale 2026", required: true }), _jsx(Input, { label: "Slug", value: values.slug, onChange: (e) => { setSlugManual(true); onChange({ slug: e.target.value }); }, placeholder: "event-charizard-flash-sale-2026", helperText: "Auto-generated from title. Must start with 'event-'." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { children: "Description" }), _jsx(RichTextEditor, { value: values.description, onChange: (v) => onChange({ description: v }), minHeightClassName: "min-h-[120px]", placeholder: "Describe this event\u2026" })] }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Input, { label: "Starts at", type: "datetime-local", value: values.startsAt, onChange: (e) => onChange({ startsAt: e.target.value }), required: true }), _jsx(Input, { label: "Ends at", type: "datetime-local", value: values.endsAt, onChange: (e) => onChange({ endsAt: e.target.value }), required: true })] }), _jsx(TagInput, { label: "Tags", value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, sale, 2026" })] })),
|
|
413
|
+
}, placeholder: "Charizard Flash Sale 2026", required: true }), _jsx(Input, { label: "Slug", error: errors.slug, value: values.slug, onChange: (e) => { setSlugManual(true); onChange({ slug: e.target.value }); }, placeholder: "event-charizard-flash-sale-2026", helperText: "Auto-generated from title. Must start with 'event-'." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { children: "Description" }), _jsx(RichTextEditor, { value: values.description, onChange: (v) => onChange({ description: v }), minHeightClassName: "min-h-[120px]", placeholder: "Describe this event\u2026" })] }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Input, { label: "Starts at", error: errors.startsAt, type: "datetime-local", value: values.startsAt, onChange: (e) => onChange({ startsAt: e.target.value }), required: true }), _jsx(Input, { label: "Ends at", error: errors.endsAt, type: "datetime-local", value: values.endsAt, onChange: (e) => onChange({ endsAt: e.target.value }), required: true })] }), _jsx(TagInput, { label: "Tags", value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, sale, 2026" })] })),
|
|
414
414
|
},
|
|
415
415
|
{
|
|
416
416
|
id: "media",
|
|
@@ -419,7 +419,7 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
419
419
|
// section mid-upload discards the transfer.
|
|
420
420
|
keepMounted: true,
|
|
421
421
|
fields: ["coverImageUrl"],
|
|
422
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(Input, { label: "Cover Image URL (optional)", value: values.coverImageUrl, onChange: (e) => onChange({ coverImageUrl: e.target.value }), placeholder: "https://\u2026", helperText: "Direct link to the event cover image." })] })),
|
|
422
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(Input, { label: "Cover Image URL (optional)", error: errors.coverImageUrl, value: values.coverImageUrl, onChange: (e) => onChange({ coverImageUrl: e.target.value }), placeholder: "https://\u2026", helperText: "Direct link to the event cover image." })] })),
|
|
423
423
|
},
|
|
424
424
|
{
|
|
425
425
|
id: "settings",
|
|
@@ -428,17 +428,19 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
428
428
|
// Same: the offer-coupon, offer-display-code and poll-minimum-options
|
|
429
429
|
// rules are all stated in eventDraftSchema's superRefine, which now
|
|
430
430
|
// surfaces them inline on the field instead of as a step-blocking banner.
|
|
431
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Event Settings" }), isEdit && (_jsx(Select, { label: "Status", value: values.status, options: EVENT_STATUS_OPTIONS, onChange: (e) => onChange({ status: e.target.value }) })), values.createdBy && (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Created by" }), _jsx(Text, { size: "sm", color: "muted", children: values.createdBy })] })), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Toggle, { checked: values.allowGuestParticipation, onChange: (v) => onChange({ allowGuestParticipation: v }), label: "Allow guest participation (no login required)" }), _jsx(Text, { size: "xs", color: "muted", children: "When enabled, unauthenticated visitors can participate in this event (poll vote, survey/feedback response, raffle entry, or wheel spin), identified by a hashed IP address instead of an account. Applies uniformly to whatever entry mechanism this event type uses." })] }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Toggle, { checked: values.isPinned, onChange: (v) => onChange({ isPinned: v }), label: "Pin to the top of Events" }), _jsx(Text, { size: "xs", color: "muted", children: "Pinned events are listed first on the Events page and in the homepage strip, ahead of the usual soonest-first order. A pinned event still has to be Active to appear at all \u2014 pinning does not publish it." })] }), values.type === "sale" && (_jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Sale configuration" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Input, { label: "Discount %", type: "number", value: values.discountPercent, onChange: (e) => onChange({ discountPercent: e.target.value }), placeholder: "10" }), _jsx(Input, { label: "Banner text (optional)", value: values.saleBannerText, onChange: (e) => onChange({ saleBannerText: e.target.value }), placeholder: "Limited time \u2014 ends Sunday!" })] })] })), values.type === "offer" && (_jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Offer configuration" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Coupon" }), _jsx(CouponInlineSelect, { scope: "admin", value: values.couponId, onChange: (id) => onChange({ couponId: id ?? "" }), placeholder: "Search coupons\u2026", allowCreate: false })] }), _jsx(Input, { label: "Display code", value: values.displayCode, onChange: (e) => onChange({ displayCode: e.target.value }), placeholder: "CHARIZARD25", required: true })] }), _jsx(Input, { label: "Banner text (optional)", value: values.offerBannerText, onChange: (e) => onChange({ offerBannerText: e.target.value }), placeholder: "Use CHARIZARD25 at checkout" })] })), values.type === "poll" && (_jsxs(Stack, { gap: "md", rounded: "xl", border: "default", padding: "md", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Poll configuration" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Options (minimum 2)" }), values.pollOptions.map((opt, idx) => (_jsxs(Row, { gap: "xs", children: [_jsx(Div, { className: "flex-1", children: _jsx(Input, { value: opt.label,
|
|
431
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Event Settings" }), isEdit && (_jsx(Select, { label: "Status", error: errors.status, value: values.status, options: EVENT_STATUS_OPTIONS, onChange: (e) => onChange({ status: e.target.value }) })), values.createdBy && (_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Created by" }), _jsx(Text, { size: "sm", color: "muted", children: values.createdBy })] })), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Toggle, { checked: values.allowGuestParticipation, onChange: (v) => onChange({ allowGuestParticipation: v }), label: "Allow guest participation (no login required)" }), _jsx(Text, { size: "xs", color: "muted", children: "When enabled, unauthenticated visitors can participate in this event (poll vote, survey/feedback response, raffle entry, or wheel spin), identified by a hashed IP address instead of an account. Applies uniformly to whatever entry mechanism this event type uses." })] }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Toggle, { checked: values.isPinned, onChange: (v) => onChange({ isPinned: v }), label: "Pin to the top of Events" }), _jsx(Text, { size: "xs", color: "muted", children: "Pinned events are listed first on the Events page and in the homepage strip, ahead of the usual soonest-first order. A pinned event still has to be Active to appear at all \u2014 pinning does not publish it." })] }), values.type === "sale" && (_jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Sale configuration" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Input, { label: "Discount %", error: errors.discountPercent, type: "number", value: values.discountPercent, onChange: (e) => onChange({ discountPercent: e.target.value }), placeholder: "10" }), _jsx(Input, { label: "Banner text (optional)", value: values.saleBannerText, error: errors.saleBannerText, onChange: (e) => onChange({ saleBannerText: e.target.value }), placeholder: "Limited time \u2014 ends Sunday!" })] })] })), values.type === "offer" && (_jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Offer configuration" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Coupon" }), _jsx(CouponInlineSelect, { scope: "admin", value: values.couponId, onChange: (id) => onChange({ couponId: id ?? "" }), placeholder: "Search coupons\u2026", allowCreate: false })] }), _jsx(Input, { label: "Display code", error: errors.displayCode, value: values.displayCode, onChange: (e) => onChange({ displayCode: e.target.value }), placeholder: "CHARIZARD25", required: true })] }), _jsx(Input, { label: "Banner text (optional)", value: values.offerBannerText, error: errors.offerBannerText, onChange: (e) => onChange({ offerBannerText: e.target.value }), placeholder: "Use CHARIZARD25 at checkout" })] })), values.type === "poll" && (_jsxs(Stack, { gap: "md", rounded: "xl", border: "default", padding: "md", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Poll configuration" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", color: "muted", children: "Options (minimum 2)" }), values.pollOptions.map((opt, idx) => (_jsxs(Row, { gap: "xs", children: [_jsx(Div, { className: "flex-1", children: _jsx(Input, { value: opt.label,
|
|
432
|
+
/* Indexed: a failure on option 2 arrives as `pollOptions.1.label`. */
|
|
433
|
+
error: errors[`pollOptions.${idx}.label`], onChange: (e) => onChange({ pollOptions: values.pollOptions.map((o) => o.id === opt.id ? { ...o, label: e.target.value } : o) }), placeholder: `Option ${idx + 1}` }) }), values.pollOptions.length > 2 && (_jsx(Button, { variant: "ghost", type: "button", onClick: () => onChange({ pollOptions: values.pollOptions.filter((o) => o.id !== opt.id) }), className: CLS_REMOVE_BTN_PX2, "aria-label": "Remove option", children: "\u00D7" }))] }, opt.id))), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onChange({ pollOptions: [...values.pollOptions, { id: `opt${Date.now()}`, label: "" }] }), children: "+ Add option" })] }), _jsx(Select, { label: "Results visibility", error: errors.resultsVisibility, value: values.resultsVisibility, options: POLL_VISIBILITY_OPTIONS, onChange: (e) => onChange({ resultsVisibility: e.target.value }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Toggle, { checked: values.allowMultiSelect, onChange: (v) => onChange({ allowMultiSelect: v }), label: "Allow multi-select" }), _jsx(Toggle, { checked: values.allowComment, onChange: (v) => onChange({ allowComment: v }), label: "Allow comment with vote" })] })] })), values.type === "survey" && (_jsxs(Stack, { gap: "md", rounded: "xl", border: "default", padding: "md", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Survey configuration" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Input, { label: "Max entries per user", error: errors.maxEntriesPerUser, type: "number", value: values.maxEntriesPerUser, onChange: (e) => onChange({ maxEntriesPerUser: e.target.value }), placeholder: "1" }), values.hasPointSystem && (_jsx(Input, { label: "Points label", error: errors.pointsLabel, value: values.pointsLabel, onChange: (e) => onChange({ pointsLabel: e.target.value }), placeholder: "Points" }))] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Toggle, { checked: values.requireLogin, onChange: (v) => onChange({ requireLogin: v }), label: "Require login to participate" }), _jsx(Toggle, { checked: values.hasLeaderboard, onChange: (v) => onChange({ hasLeaderboard: v }), label: "Show leaderboard" }), _jsx(Toggle, { checked: values.hasPointSystem, onChange: (v) => onChange({ hasPointSystem: v }), label: "Enable point system" }), _jsx(Toggle, { checked: values.entryReviewRequired, onChange: (v) => onChange({ entryReviewRequired: v }), label: "Require employee review before entry is approved" })] }), _jsx(FormFieldBuilder, { fields: values.surveyFields, setFields: (f) => onChange({ surveyFields: f }) })] })), values.type === "feedback" && (_jsxs(Stack, { gap: "md", rounded: "xl", border: "default", padding: "md", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "Feedback configuration" }), _jsx(Toggle, { checked: values.anonymous, onChange: (v) => onChange({ anonymous: v }), label: "Allow anonymous submissions" }), _jsx(FormFieldBuilder, { fields: values.feedbackFields, setFields: (f) => onChange({ feedbackFields: f }) })] }))] })),
|
|
432
434
|
},
|
|
433
435
|
{
|
|
434
436
|
id: "raffle",
|
|
435
437
|
label: "Raffle / Spin",
|
|
436
438
|
fields: ["hasRaffle", "raffleType", "raffleTopN", "rafflePrize", "rafflePrizeCouponId", "rafflePrizeProductIds", "spinPrizes", "spinMaxPerUser", "spinWindowStart", "spinWindowEnd"],
|
|
437
|
-
render: ({ values, onChange }) => {
|
|
439
|
+
render: ({ values, onChange, errors }) => {
|
|
438
440
|
const isRaffleType = values.type === "raffle" || values.type === "spin_wheel";
|
|
439
441
|
const showRaffleConfig = isRaffleType || values.hasRaffle;
|
|
440
442
|
const isSpinMode = values.type === "spin_wheel" || values.raffleType === "spin_wheel";
|
|
441
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Raffle & Spin Wheel" }), !isRaffleType && (_jsx(Toggle, { checked: values.hasRaffle, onChange: (v) => onChange({ hasRaffle: v }), label: "Attach a raffle to this event" })), !isRaffleType && !values.hasRaffle && (_jsx(Text, { size: "sm", color: "muted", children: "Enable the toggle above to attach a raffle draw to this event. Not applicable for this event type by default." })), showRaffleConfig && (_jsxs(Stack, { gap: "md", rounded: "xl", border: "default", padding: "md", children: [_jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Select, { label: "Raffle type", value: values.type === "spin_wheel" ? "spin_wheel" : values.raffleType, options: RAFFLE_TYPE_OPTIONS, onChange: (e) => onChange({ raffleType: e.target.value }), disabled: values.type === "spin_wheel" }), _jsx(Input, { label: "Top N (for top-N raffle types)", type: "number", value: values.raffleTopN, onChange: (e) => onChange({ raffleTopN: e.target.value }), placeholder: "10" })] }), _jsx(Input, { label: "Prize description", value: values.rafflePrize, onChange: (e) => onChange({ rafflePrize: e.target.value }), placeholder: "\u20B92,000 store credit + exclusive Pikachu sticker" }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Coupon for winner (optional)" }), _jsx(CouponInlineSelect, { scope: "admin", value: values.rafflePrizeCouponId, onChange: (id) => onChange({ rafflePrizeCouponId: id ?? "" }), placeholder: "Search coupons\u2026", allowCreate: false })] }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Prize products (optional)" }), _jsx(ProductInlineSelect, { scope: "admin", multiple: true, value: values.rafflePrizeProductIds, onChange: (ids) => onChange({ rafflePrizeProductIds: ids }), placeholder: "Search products to feature as prizes\u2026" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-1", children: "Products shown to winners or displayed as the prize showcase." })] }), isSpinMode && _jsx(SpinPrizesEditor, { values: values, onChange: onChange }), isEdit && (_jsxs(Stack, { gap: "xs", rounded: "lg", padding: "sm", className: CLS_RAFFLE_PANEL, children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", className: CLS_RAFFLE_HEADING, children: raffleWinnerName ? `Winner: ${raffleWinnerName}` : "Raffle not yet triggered" }), _jsx(Button, { type: "button", action: ACTIONS.EVENT["draw-raffle-winner"], size: "sm", onClick: () => triggerRaffleMutation.mutate(), disabled: triggerRaffleMutation.isPending || Boolean(raffleWinnerName), children: triggerRaffleMutation.isPending ? "Triggering…" : raffleWinnerName ? "Already triggered" : "Trigger Raffle Now" })] }), raffleEntryCount !== null && _jsxs(Text, { size: "xs", className: CLS_RAFFLE_BODY, children: ["Pool size: ", raffleEntryCount] }), raffleGithubUrl && _jsxs(Text, { size: "xs", className: `break-all ${CLS_RAFFLE_BODY}`, children: ["Fairness proof: ", raffleGithubUrl] }), triggerMessage && _jsx(Text, { size: "xs", className: CLS_RAFFLE_HEADING, children: triggerMessage })] }))] }))] }));
|
|
443
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Raffle & Spin Wheel" }), !isRaffleType && (_jsx(Toggle, { checked: values.hasRaffle, onChange: (v) => onChange({ hasRaffle: v }), label: "Attach a raffle to this event" })), !isRaffleType && !values.hasRaffle && (_jsx(Text, { size: "sm", color: "muted", children: "Enable the toggle above to attach a raffle draw to this event. Not applicable for this event type by default." })), showRaffleConfig && (_jsxs(Stack, { gap: "md", rounded: "xl", border: "default", padding: "md", children: [_jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(Select, { label: "Raffle type", error: errors.raffleType, value: values.type === "spin_wheel" ? "spin_wheel" : values.raffleType, options: RAFFLE_TYPE_OPTIONS, onChange: (e) => onChange({ raffleType: e.target.value }), disabled: values.type === "spin_wheel" }), _jsx(Input, { label: "Top N (for top-N raffle types)", error: errors.raffleTopN, type: "number", value: values.raffleTopN, onChange: (e) => onChange({ raffleTopN: e.target.value }), placeholder: "10" })] }), _jsx(Input, { label: "Prize description", error: errors.rafflePrize, value: values.rafflePrize, onChange: (e) => onChange({ rafflePrize: e.target.value }), placeholder: "\u20B92,000 store credit + exclusive Pikachu sticker" }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Coupon for winner (optional)" }), _jsx(CouponInlineSelect, { scope: "admin", value: values.rafflePrizeCouponId, onChange: (id) => onChange({ rafflePrizeCouponId: id ?? "" }), placeholder: "Search coupons\u2026", allowCreate: false })] }), _jsxs(Div, { children: [_jsx(Text, { size: "sm", weight: "medium", className: "mb-1", children: "Prize products (optional)" }), _jsx(ProductInlineSelect, { scope: "admin", multiple: true, value: values.rafflePrizeProductIds, onChange: (ids) => onChange({ rafflePrizeProductIds: ids }), placeholder: "Search products to feature as prizes\u2026" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-1", children: "Products shown to winners or displayed as the prize showcase." })] }), isSpinMode && _jsx(SpinPrizesEditor, { values: values, onChange: onChange }), isEdit && (_jsxs(Stack, { gap: "xs", rounded: "lg", padding: "sm", className: CLS_RAFFLE_PANEL, children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", className: CLS_RAFFLE_HEADING, children: raffleWinnerName ? `Winner: ${raffleWinnerName}` : "Raffle not yet triggered" }), _jsx(Button, { type: "button", action: ACTIONS.EVENT["draw-raffle-winner"], size: "sm", onClick: () => triggerRaffleMutation.mutate(), disabled: triggerRaffleMutation.isPending || Boolean(raffleWinnerName), children: triggerRaffleMutation.isPending ? "Triggering…" : raffleWinnerName ? "Already triggered" : "Trigger Raffle Now" })] }), raffleEntryCount !== null && _jsxs(Text, { size: "xs", className: CLS_RAFFLE_BODY, children: ["Pool size: ", raffleEntryCount] }), raffleGithubUrl && _jsxs(Text, { size: "xs", className: `break-all ${CLS_RAFFLE_BODY}`, children: ["Fairness proof: ", raffleGithubUrl] }), triggerMessage && _jsx(Text, { size: "xs", className: CLS_RAFFLE_HEADING, children: triggerMessage })] }))] }))] }));
|
|
442
444
|
},
|
|
443
445
|
},
|
|
444
446
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { FAQ, FAQCategory } from "../types";
|
|
3
|
+
import { type PublishedValues } from "../../../_internal/shared/content/published-values";
|
|
3
4
|
export interface FAQAccordionProps {
|
|
4
5
|
faqs: FAQ[];
|
|
5
6
|
className?: string;
|
|
@@ -8,8 +9,19 @@ export interface FAQAccordionProps {
|
|
|
8
9
|
labels?: {
|
|
9
10
|
noResults?: string;
|
|
10
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Phase 9 — resolved published values, for `{{token}}` substitution in
|
|
14
|
+
* answers. Build them with `resolvePublishedValues(settings?.commissions)`
|
|
15
|
+
* at the page level and pass them down.
|
|
16
|
+
*
|
|
17
|
+
* 🛑 Passed in rather than read here. This component renders on the server
|
|
18
|
+
* and in the client, and a settings read inside a presentational component
|
|
19
|
+
* would both make it unusable in half the places it is needed and fire one
|
|
20
|
+
* read per accordion on a page that has several.
|
|
21
|
+
*/
|
|
22
|
+
publishedValues?: PublishedValues;
|
|
11
23
|
}
|
|
12
|
-
export declare function FAQAccordion({ faqs, className, renderExpandedFooter, labels, }: FAQAccordionProps): React.JSX.Element;
|
|
24
|
+
export declare function FAQAccordion({ faqs, className, renderExpandedFooter, labels, publishedValues, }: FAQAccordionProps): React.JSX.Element;
|
|
13
25
|
interface FAQCategoryTabsProps {
|
|
14
26
|
categories: FAQCategory[];
|
|
15
27
|
active?: FAQCategory | null;
|
|
@@ -2,16 +2,41 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Div, RichText, Row, Span, Tabs, TabsList, TabsTrigger, Text } from "../../../ui/index.js";
|
|
5
|
+
/* Phase 9 — `publishedCopyKey`'s first reader. See the 🛑 at the render site. */
|
|
6
|
+
import { resolvePublishedCopy, resolvePublishedValues, } from "../../../_internal/shared/content/published-values.js";
|
|
5
7
|
import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Code-derived values, resolved once at module scope.
|
|
10
|
+
*
|
|
11
|
+
* ## 🛑 Why a DEFAULT rather than a required prop
|
|
12
|
+
*
|
|
13
|
+
* `resolvePublishedValues()` with no settings argument is PURE over code
|
|
14
|
+
* constants — the obligation policies, `LANE_CHECKOUT_WINDOW_MS`,
|
|
15
|
+
* `POINTS_EARN_PERCENT`. It reads no Firestore and touches no settings, so
|
|
16
|
+
* calling it here costs one object at import and cannot leak anything.
|
|
17
|
+
*
|
|
18
|
+
* That matters because there are FOUR mount sites (the FAQ page, the
|
|
19
|
+
* searchable list, the category highlights section, and this file's own
|
|
20
|
+
* loop). Requiring the prop would mean threading it through four callers and
|
|
21
|
+
* their callers — and the one that got missed would render `{{…}}` to a
|
|
22
|
+
* visitor. A default that always resolves the code-derived numbers, with an
|
|
23
|
+
* override for the settings-driven money ones, gets every surface right
|
|
24
|
+
* immediately.
|
|
25
|
+
*
|
|
26
|
+
* A caller WITH settings in hand should still pass them: without them the
|
|
27
|
+
* money values fall back to the schema defaults, which is correct but is not
|
|
28
|
+
* the admin's current configuration.
|
|
29
|
+
*/
|
|
30
|
+
const CODE_DERIVED_VALUES = resolvePublishedValues();
|
|
31
|
+
function FAQAccordionItem({ faq, isOpen, onToggle, renderExpandedFooter, publishedValues = CODE_DERIVED_VALUES, }) {
|
|
32
|
+
return (_jsxs(Div, { className: "border-b border-neutral-200 last:border-0", children: [_jsxs(Button, { variant: "ghost", onClick: onToggle, "aria-expanded": isOpen, justify: "between", className: "w-full gap-[1rem] py-[1rem] text-left text-[0.875rem] font-[500] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] transition hover:text-primary", children: [_jsx(Span, { children: faq.question }), _jsx(Span, { className: `flex-shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), isOpen && (_jsxs(Div, { textSize: "sm", className: "text-[var(--appkit-color-text-muted)]", padding: "b-md", children: [faq.tags && faq.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-3", children: faq.tags.map((tag) => (_jsx(Span, { className: "bg-[var(--appkit-color-surface)] py-[0.25rem] text-[var(--appkit-color-text-muted)]", rounded: "md", padding: "x-xs", size: "xs", children: tag }, tag))) })), faq.answer.format === "html" ? (_jsx(RichText, { html: resolvePublishedCopy(faq.answer.text, publishedValues), className: "prose-sm" })) : (_jsx(Text, { className: "whitespace-pre-line", children: resolvePublishedCopy(faq.answer.text, publishedValues) })), _jsx(Row, { justify: "end", className: "mt-3", children: _jsx(RecordOwnerQuickActions, { resource: "FAQ", recordId: faq.id, placement: "detail", recordLabel: faq.question }) }), renderExpandedFooter?.(faq)] }))] }));
|
|
8
33
|
}
|
|
9
|
-
export function FAQAccordion({ faqs, className = "", renderExpandedFooter, labels, }) {
|
|
34
|
+
export function FAQAccordion({ faqs, className = "", renderExpandedFooter, labels, publishedValues, }) {
|
|
10
35
|
const [openId, setOpenId] = useState(null);
|
|
11
36
|
if (faqs.length === 0 && labels?.noResults) {
|
|
12
37
|
return (_jsx(Div, { surface: "card", padding: "xl", className: "text-center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: labels.noResults }) }));
|
|
13
38
|
}
|
|
14
|
-
return (_jsx(Div, { surface: "card", className: `divide-y divide-neutral-100 divide-[var(--appkit-color-border)] ${className}`, padding: "x-md", children: faqs.map((faq) => (_jsx(FAQAccordionItem, { faq: faq, isOpen: openId === faq.id, onToggle: () => setOpenId(openId === faq.id ? null : faq.id), renderExpandedFooter: renderExpandedFooter }, faq.id))) }));
|
|
39
|
+
return (_jsx(Div, { surface: "card", className: `divide-y divide-neutral-100 divide-[var(--appkit-color-border)] ${className}`, padding: "x-md", children: faqs.map((faq) => (_jsx(FAQAccordionItem, { faq: faq, isOpen: openId === faq.id, onToggle: () => setOpenId(openId === faq.id ? null : faq.id), renderExpandedFooter: renderExpandedFooter, publishedValues: publishedValues }, faq.id))) }));
|
|
15
40
|
}
|
|
16
41
|
const ALL_TAB_VALUE = "__all__";
|
|
17
42
|
export function FAQCategoryTabs({ categories, active, onSelect, labels = {}, }) {
|
|
@@ -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()
|