@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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocating a number from a gapless series (Phase 7, D12).
|
|
3
|
+
*
|
|
4
|
+
* ## The whole design in one paragraph
|
|
5
|
+
*
|
|
6
|
+
* One transaction does three things: read the (series, FY) counter, `.create()`
|
|
7
|
+
* an allocation claim keyed on the subject, and write `lastNumber + 1`. Any one
|
|
8
|
+
* of the three failing rolls back all of them, so there is no state where a
|
|
9
|
+
* number was consumed without being recorded, and no state where a subject
|
|
10
|
+
* holds two numbers.
|
|
11
|
+
*
|
|
12
|
+
* ## 🛑 Why the claim is INSIDE the transaction
|
|
13
|
+
*
|
|
14
|
+
* The obvious shape is "check whether this order already has a number, then
|
|
15
|
+
* allocate". That is a read-then-write race: two concurrent triggers both read
|
|
16
|
+
* "no number", both allocate, and the order ends up with two — one of which is
|
|
17
|
+
* a permanent gap in a series an auditor reads.
|
|
18
|
+
*
|
|
19
|
+
* `.create()` inside the transaction makes it impossible rather than unlikely.
|
|
20
|
+
* A duplicate attempt fails the transaction, and the caller treats that as
|
|
21
|
+
* "already allocated" and reads the existing number back. This is the fourth
|
|
22
|
+
* use of that primitive in this codebase (refund claims, the gateway-event
|
|
23
|
+
* inbox, the gateway-order claim), and the one with the least recoverable
|
|
24
|
+
* failure mode.
|
|
25
|
+
*
|
|
26
|
+
* ## 🛑 Allocate on `paymentStatus → paid`, never at order creation
|
|
27
|
+
*
|
|
28
|
+
* COD and manual orders are created `pending`. Numbering at creation burns a
|
|
29
|
+
* number on every abandoned order, and *gapless* means those gaps are
|
|
30
|
+
* unexplainable — "that one was abandoned" is not an answer an auditor accepts
|
|
31
|
+
* for a missing invoice number.
|
|
32
|
+
*/
|
|
33
|
+
import { type InvoiceSeriesKind } from "../../../../features/invoicing/schemas/firestore";
|
|
34
|
+
export interface AllocateSeriesNumberInput {
|
|
35
|
+
kind: InvoiceSeriesKind;
|
|
36
|
+
/**
|
|
37
|
+
* What the number is FOR, and the idempotency key.
|
|
38
|
+
*
|
|
39
|
+
* An invoice passes the order id. A credit note passes
|
|
40
|
+
* `${orderId}_${refundId}`, because an order may have several.
|
|
41
|
+
*/
|
|
42
|
+
subjectId: string;
|
|
43
|
+
orderId: string;
|
|
44
|
+
/** Defaults to now. Injectable so a backfill can issue into a past FY. */
|
|
45
|
+
at?: Date;
|
|
46
|
+
}
|
|
47
|
+
export interface AllocateSeriesNumberResult {
|
|
48
|
+
number: string;
|
|
49
|
+
fy: string;
|
|
50
|
+
sequence: number;
|
|
51
|
+
/** True when this call did the allocating; false when it found an existing one. */
|
|
52
|
+
allocated: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Allocate (or return the already-allocated) number for one subject.
|
|
56
|
+
*
|
|
57
|
+
* 🛑 Idempotent and NOT best-effort. It throws rather than returning a
|
|
58
|
+
* sentinel, because every caller's correct response to "we could not number
|
|
59
|
+
* this" is to leave the order un-invoiced and retry — a caller that treated
|
|
60
|
+
* failure as "no number needed" would produce a paid order with no invoice,
|
|
61
|
+
* silently.
|
|
62
|
+
*/
|
|
63
|
+
export declare function allocateSeriesNumber(input: AllocateSeriesNumberInput): Promise<AllocateSeriesNumberResult>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocating a number from a gapless series (Phase 7, D12).
|
|
3
|
+
*
|
|
4
|
+
* ## The whole design in one paragraph
|
|
5
|
+
*
|
|
6
|
+
* One transaction does three things: read the (series, FY) counter, `.create()`
|
|
7
|
+
* an allocation claim keyed on the subject, and write `lastNumber + 1`. Any one
|
|
8
|
+
* of the three failing rolls back all of them, so there is no state where a
|
|
9
|
+
* number was consumed without being recorded, and no state where a subject
|
|
10
|
+
* holds two numbers.
|
|
11
|
+
*
|
|
12
|
+
* ## 🛑 Why the claim is INSIDE the transaction
|
|
13
|
+
*
|
|
14
|
+
* The obvious shape is "check whether this order already has a number, then
|
|
15
|
+
* allocate". That is a read-then-write race: two concurrent triggers both read
|
|
16
|
+
* "no number", both allocate, and the order ends up with two — one of which is
|
|
17
|
+
* a permanent gap in a series an auditor reads.
|
|
18
|
+
*
|
|
19
|
+
* `.create()` inside the transaction makes it impossible rather than unlikely.
|
|
20
|
+
* A duplicate attempt fails the transaction, and the caller treats that as
|
|
21
|
+
* "already allocated" and reads the existing number back. This is the fourth
|
|
22
|
+
* use of that primitive in this codebase (refund claims, the gateway-event
|
|
23
|
+
* inbox, the gateway-order claim), and the one with the least recoverable
|
|
24
|
+
* failure mode.
|
|
25
|
+
*
|
|
26
|
+
* ## 🛑 Allocate on `paymentStatus → paid`, never at order creation
|
|
27
|
+
*
|
|
28
|
+
* COD and manual orders are created `pending`. Numbering at creation burns a
|
|
29
|
+
* number on every abandoned order, and *gapless* means those gaps are
|
|
30
|
+
* unexplainable — "that one was abandoned" is not an answer an auditor accepts
|
|
31
|
+
* for a missing invoice number.
|
|
32
|
+
*/
|
|
33
|
+
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
34
|
+
import { serverLogger } from "../../../../monitoring/index.js";
|
|
35
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
36
|
+
import { INVOICE_ALLOCATION_COLLECTION, INVOICE_COUNTER_COLLECTION, } from "../../../../features/invoicing/schemas/firestore.js";
|
|
37
|
+
import { CREDIT_NOTE_PREFIX, INVOICE_PREFIX, financialYearLabel, formatSeriesNumber, } from "../../../../features/invoicing/utils/financial-year.js";
|
|
38
|
+
function prefixFor(kind) {
|
|
39
|
+
return kind === "credit_note" ? CREDIT_NOTE_PREFIX : INVOICE_PREFIX;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Allocate (or return the already-allocated) number for one subject.
|
|
43
|
+
*
|
|
44
|
+
* 🛑 Idempotent and NOT best-effort. It throws rather than returning a
|
|
45
|
+
* sentinel, because every caller's correct response to "we could not number
|
|
46
|
+
* this" is to leave the order un-invoiced and retry — a caller that treated
|
|
47
|
+
* failure as "no number needed" would produce a paid order with no invoice,
|
|
48
|
+
* silently.
|
|
49
|
+
*/
|
|
50
|
+
export async function allocateSeriesNumber(input) {
|
|
51
|
+
const db = getAdminDb();
|
|
52
|
+
const at = input.at ?? new Date();
|
|
53
|
+
const fy = financialYearLabel(at);
|
|
54
|
+
const counterRef = db
|
|
55
|
+
.collection(INVOICE_COUNTER_COLLECTION)
|
|
56
|
+
.doc(`${input.kind}_${fy}`);
|
|
57
|
+
const claimRef = db.collection(INVOICE_ALLOCATION_COLLECTION).doc(input.subjectId);
|
|
58
|
+
try {
|
|
59
|
+
return await db.runTransaction(async (tx) => {
|
|
60
|
+
/*
|
|
61
|
+
* 🛑 Both reads BEFORE any write — a Firestore transaction requires it,
|
|
62
|
+
* and the claim read is what lets an already-allocated subject return
|
|
63
|
+
* its existing number instead of failing.
|
|
64
|
+
*/
|
|
65
|
+
const [claimSnap, counterSnap] = await Promise.all([
|
|
66
|
+
tx.get(claimRef),
|
|
67
|
+
tx.get(counterRef),
|
|
68
|
+
]);
|
|
69
|
+
if (claimSnap.exists) {
|
|
70
|
+
const existing = claimSnap.data();
|
|
71
|
+
return {
|
|
72
|
+
number: existing.number,
|
|
73
|
+
fy: existing.fy,
|
|
74
|
+
sequence: existing.sequence,
|
|
75
|
+
allocated: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const last = counterSnap.exists
|
|
79
|
+
? (counterSnap.data().lastNumber ?? 0)
|
|
80
|
+
: 0;
|
|
81
|
+
const sequence = last + 1;
|
|
82
|
+
const number = formatSeriesNumber(prefixFor(input.kind), fy, sequence);
|
|
83
|
+
/*
|
|
84
|
+
* `set(..., { merge: true })` on the counter rather than `update`,
|
|
85
|
+
* because the first allocation of a financial year creates it — and
|
|
86
|
+
* `update` on a missing document throws, which would make 1 April the
|
|
87
|
+
* one day of the year invoicing breaks.
|
|
88
|
+
*/
|
|
89
|
+
tx.set(counterRef, {
|
|
90
|
+
kind: input.kind,
|
|
91
|
+
fy,
|
|
92
|
+
lastNumber: sequence,
|
|
93
|
+
lastIssuedAt: at,
|
|
94
|
+
updatedAt: at,
|
|
95
|
+
...(counterSnap.exists ? {} : { createdAt: at }),
|
|
96
|
+
}, { merge: true });
|
|
97
|
+
/*
|
|
98
|
+
* 🛑 `create`, not `set`. Within a transaction both are atomic against
|
|
99
|
+
* the counter, but `create` additionally refuses if the claim appeared
|
|
100
|
+
* between our read and our write — which is exactly the window a
|
|
101
|
+
* concurrent trigger occupies.
|
|
102
|
+
*/
|
|
103
|
+
tx.create(claimRef, {
|
|
104
|
+
kind: input.kind,
|
|
105
|
+
subjectId: input.subjectId,
|
|
106
|
+
orderId: input.orderId,
|
|
107
|
+
number,
|
|
108
|
+
fy,
|
|
109
|
+
sequence,
|
|
110
|
+
issuedAt: at,
|
|
111
|
+
createdAt: at,
|
|
112
|
+
updatedAt: at,
|
|
113
|
+
});
|
|
114
|
+
return { number, fy, sequence, allocated: true };
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const normalized = normalizeError(err);
|
|
119
|
+
/*
|
|
120
|
+
* A lost race: the other side created the claim first. Read it back rather
|
|
121
|
+
* than failing — both callers want the same answer, and whoever wrote it
|
|
122
|
+
* wrote the number this subject now permanently has.
|
|
123
|
+
*/
|
|
124
|
+
/*
|
|
125
|
+
* Logged, not swallowed. `.catch(() => null)` alone made a Firestore
|
|
126
|
+
* failure indistinguishable from "the claim genuinely is not there" — and
|
|
127
|
+
* the two take opposite branches below: one must fall through to a fresh
|
|
128
|
+
* allocation, the other must NOT, because re-allocating over a claim that
|
|
129
|
+
* exists is how an invoice number gets issued twice. Root Cause #59's
|
|
130
|
+
* shape, in the one place in this codebase where a silent degrade would
|
|
131
|
+
* duplicate a filed document number.
|
|
132
|
+
*/
|
|
133
|
+
const after = await claimRef.get().catch((readErr) => {
|
|
134
|
+
const e = normalizeError(readErr);
|
|
135
|
+
serverLogger.error("[invoicing] could not re-read the allocation claim after a race", {
|
|
136
|
+
collection: INVOICE_ALLOCATION_COLLECTION,
|
|
137
|
+
subjectId: input.subjectId,
|
|
138
|
+
error: e.message,
|
|
139
|
+
});
|
|
140
|
+
return null;
|
|
141
|
+
});
|
|
142
|
+
if (after?.exists) {
|
|
143
|
+
const existing = after.data();
|
|
144
|
+
serverLogger.info("[invoicing] allocation race resolved to the existing number", {
|
|
145
|
+
subjectId: input.subjectId,
|
|
146
|
+
number: existing.number,
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
number: existing.number,
|
|
150
|
+
fy: existing.fy,
|
|
151
|
+
sequence: existing.sequence,
|
|
152
|
+
allocated: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
serverLogger.error("[invoicing] allocation failed", {
|
|
156
|
+
subjectId: input.subjectId,
|
|
157
|
+
kind: input.kind,
|
|
158
|
+
error: normalized.message,
|
|
159
|
+
});
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit notes — the document that reverses tax on a refund (Phase 7, 7c).
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 A refund needs a credit note; it never needs a re-issued invoice
|
|
5
|
+
*
|
|
6
|
+
* A GST invoice is issued once. When money goes back, the reversal is its own
|
|
7
|
+
* numbered document referencing the original invoice — which is why there are
|
|
8
|
+
* two series rather than a revision suffix on one.
|
|
9
|
+
*
|
|
10
|
+
* ## 🛑 Numbered on SETTLEMENT, not on reservation
|
|
11
|
+
*
|
|
12
|
+
* `processRefundAction` is two-phase: it reserves an entry, calls the provider,
|
|
13
|
+
* then settles to `processed` or `failed`. A credit note numbered at
|
|
14
|
+
* reservation would leave a numbered reversal in the series for money that
|
|
15
|
+
* never moved, on every provider failure — and the number could not be
|
|
16
|
+
* withdrawn.
|
|
17
|
+
*
|
|
18
|
+
* So this is called from the SETTLE step, for `processed` only.
|
|
19
|
+
*
|
|
20
|
+
* ## One per refund event, keyed accordingly
|
|
21
|
+
*
|
|
22
|
+
* An order can be refunded several times. The allocation claim is therefore
|
|
23
|
+
* `${orderId}_${refundId}` — keying on the order alone would refuse the second
|
|
24
|
+
* credit note, which is the inverse of the invoice's rule and is why
|
|
25
|
+
* `allocateSeriesNumber` takes `subjectId` separately from `orderId`.
|
|
26
|
+
*/
|
|
27
|
+
import type { OrderDocument, OrderRefundEvent } from "../../../../features/orders/schemas/firestore";
|
|
28
|
+
/** Same private prefix family as invoices — refused by the media proxy. */
|
|
29
|
+
export declare const CREDIT_NOTE_PREFIX_PATH = "private/credit-notes";
|
|
30
|
+
/**
|
|
31
|
+
* Renders a credit note. Injected for the same reason as `InvoiceRenderer`:
|
|
32
|
+
* `pdfkit` is a Functions dependency.
|
|
33
|
+
*/
|
|
34
|
+
export type CreditNoteRenderer = (order: OrderDocument, refund: OrderRefundEvent, creditNoteNumber: string) => Promise<Buffer>;
|
|
35
|
+
export interface IssueCreditNoteResult {
|
|
36
|
+
creditNoteNumber: string;
|
|
37
|
+
creditNoteStoragePath?: string;
|
|
38
|
+
issued: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Number and (optionally) render the credit note for one settled refund.
|
|
42
|
+
*
|
|
43
|
+
* 🛑 **Best-effort by design, and that is the opposite of the invoice rule.**
|
|
44
|
+
* It is called from the refund settle path, where the money has ALREADY left
|
|
45
|
+
* the gateway. Throwing here would turn a successful refund into a failed
|
|
46
|
+
* action — the buyer has their money, our code reports an error, and an
|
|
47
|
+
* operator retries a refund that already happened. A missing credit note is a
|
|
48
|
+
* reporting gap recoverable by a backfill; a double refund is not.
|
|
49
|
+
*
|
|
50
|
+
* The invoice path throws for the mirror-image reason: nothing irreversible has
|
|
51
|
+
* happened yet there, so refusing to proceed is the safe direction.
|
|
52
|
+
*
|
|
53
|
+
* `render` is optional — passing none numbers the reversal without producing a
|
|
54
|
+
* document, which is what the GSTR-1 export actually needs (it reconciles
|
|
55
|
+
* numbers and amounts, not PDFs). The PDF is for the buyer.
|
|
56
|
+
*/
|
|
57
|
+
export declare function issueCreditNoteForRefund(order: OrderDocument, refund: OrderRefundEvent, render?: CreditNoteRenderer): Promise<IssueCreditNoteResult | null>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit notes — the document that reverses tax on a refund (Phase 7, 7c).
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 A refund needs a credit note; it never needs a re-issued invoice
|
|
5
|
+
*
|
|
6
|
+
* A GST invoice is issued once. When money goes back, the reversal is its own
|
|
7
|
+
* numbered document referencing the original invoice — which is why there are
|
|
8
|
+
* two series rather than a revision suffix on one.
|
|
9
|
+
*
|
|
10
|
+
* ## 🛑 Numbered on SETTLEMENT, not on reservation
|
|
11
|
+
*
|
|
12
|
+
* `processRefundAction` is two-phase: it reserves an entry, calls the provider,
|
|
13
|
+
* then settles to `processed` or `failed`. A credit note numbered at
|
|
14
|
+
* reservation would leave a numbered reversal in the series for money that
|
|
15
|
+
* never moved, on every provider failure — and the number could not be
|
|
16
|
+
* withdrawn.
|
|
17
|
+
*
|
|
18
|
+
* So this is called from the SETTLE step, for `processed` only.
|
|
19
|
+
*
|
|
20
|
+
* ## One per refund event, keyed accordingly
|
|
21
|
+
*
|
|
22
|
+
* An order can be refunded several times. The allocation claim is therefore
|
|
23
|
+
* `${orderId}_${refundId}` — keying on the order alone would refuse the second
|
|
24
|
+
* credit note, which is the inverse of the invoice's rule and is why
|
|
25
|
+
* `allocateSeriesNumber` takes `subjectId` separately from `orderId`.
|
|
26
|
+
*/
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { getAdminStorage } from "../../../../providers/db-firebase/index.js";
|
|
29
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
30
|
+
import { serverLogger } from "../../../../monitoring/index.js";
|
|
31
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
32
|
+
import { allocateSeriesNumber } from "./allocate.js";
|
|
33
|
+
/** Same private prefix family as invoices — refused by the media proxy. */
|
|
34
|
+
export const CREDIT_NOTE_PREFIX_PATH = "private/credit-notes";
|
|
35
|
+
/**
|
|
36
|
+
* Number and (optionally) render the credit note for one settled refund.
|
|
37
|
+
*
|
|
38
|
+
* 🛑 **Best-effort by design, and that is the opposite of the invoice rule.**
|
|
39
|
+
* It is called from the refund settle path, where the money has ALREADY left
|
|
40
|
+
* the gateway. Throwing here would turn a successful refund into a failed
|
|
41
|
+
* action — the buyer has their money, our code reports an error, and an
|
|
42
|
+
* operator retries a refund that already happened. A missing credit note is a
|
|
43
|
+
* reporting gap recoverable by a backfill; a double refund is not.
|
|
44
|
+
*
|
|
45
|
+
* The invoice path throws for the mirror-image reason: nothing irreversible has
|
|
46
|
+
* happened yet there, so refusing to proceed is the safe direction.
|
|
47
|
+
*
|
|
48
|
+
* `render` is optional — passing none numbers the reversal without producing a
|
|
49
|
+
* document, which is what the GSTR-1 export actually needs (it reconciles
|
|
50
|
+
* numbers and amounts, not PDFs). The PDF is for the buyer.
|
|
51
|
+
*/
|
|
52
|
+
export async function issueCreditNoteForRefund(order, refund, render) {
|
|
53
|
+
if (refund.creditNoteNumber) {
|
|
54
|
+
return {
|
|
55
|
+
creditNoteNumber: refund.creditNoteNumber,
|
|
56
|
+
creditNoteStoragePath: refund.creditNoteStoragePath,
|
|
57
|
+
issued: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/*
|
|
61
|
+
* 🛑 Only a refund that actually moved money gets a number. `status`
|
|
62
|
+
* undefined means "committed" to every reader (rows written before the
|
|
63
|
+
* two-phase lifecycle existed carry none), so the test admits those and
|
|
64
|
+
* refuses only an explicit `reserved` or `failed`.
|
|
65
|
+
*/
|
|
66
|
+
if (refund.status === "reserved" || refund.status === "failed") {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const issuedAt = new Date();
|
|
71
|
+
const allocation = await allocateSeriesNumber({
|
|
72
|
+
kind: "credit_note",
|
|
73
|
+
subjectId: `${order.id}_${refund.refundId}`,
|
|
74
|
+
orderId: order.id,
|
|
75
|
+
at: issuedAt,
|
|
76
|
+
});
|
|
77
|
+
let storagePath;
|
|
78
|
+
if (render) {
|
|
79
|
+
storagePath = `${CREDIT_NOTE_PREFIX_PATH}/${order.id}/${randomUUID()}.pdf`;
|
|
80
|
+
const buffer = await render(order, refund, allocation.number);
|
|
81
|
+
await getAdminStorage()
|
|
82
|
+
.bucket()
|
|
83
|
+
.file(storagePath)
|
|
84
|
+
.save(buffer, {
|
|
85
|
+
contentType: "application/pdf",
|
|
86
|
+
metadata: { cacheControl: "private, no-store" },
|
|
87
|
+
resumable: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/*
|
|
91
|
+
* 🛑 Written through the repository's own refund-event mutator, NOT by
|
|
92
|
+
* read-modify-writing `refunds[]`. That array is appended by a transaction
|
|
93
|
+
* precisely because a read-then-`[...existing, event]` write LOSES an
|
|
94
|
+
* event under concurrency (D6), and stamping a credit-note number the
|
|
95
|
+
* naive way would reintroduce exactly that defect on the same array.
|
|
96
|
+
*/
|
|
97
|
+
await orderRepository.setRefundCreditNote(order.id, refund.refundId, {
|
|
98
|
+
creditNoteNumber: allocation.number,
|
|
99
|
+
creditNoteIssuedAt: issuedAt,
|
|
100
|
+
...(storagePath ? { creditNoteStoragePath: storagePath } : {}),
|
|
101
|
+
});
|
|
102
|
+
serverLogger.info("[invoicing] credit note issued", {
|
|
103
|
+
orderId: order.id,
|
|
104
|
+
refundId: refund.refundId,
|
|
105
|
+
creditNoteNumber: allocation.number,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
creditNoteNumber: allocation.number,
|
|
109
|
+
creditNoteStoragePath: storagePath,
|
|
110
|
+
issued: allocation.allocated,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
/*
|
|
115
|
+
* Reported loudly and swallowed — see the 🛑 on this function. A refund
|
|
116
|
+
* that succeeded must not be reported as failed because its paperwork
|
|
117
|
+
* could not be numbered.
|
|
118
|
+
*/
|
|
119
|
+
serverLogger.error("[invoicing] credit note FAILED — refund stands, reporting gap", {
|
|
120
|
+
orderId: order.id,
|
|
121
|
+
refundId: refund.refundId,
|
|
122
|
+
error: normalizeError(err).message,
|
|
123
|
+
});
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSTR-1 export (Phase 7d) — the INPUT to a human filing, never a filing.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 What this is and is not
|
|
5
|
+
*
|
|
6
|
+
* It is a CSV an accountant opens, checks and uses. It is not a return, it is
|
|
7
|
+
* not filed, and nothing here talks to the GST portal — there is no webhook
|
|
8
|
+
* facility in existence and no GSP contract. Labelling it otherwise would be
|
|
9
|
+
* the most consequential lie in this codebase.
|
|
10
|
+
*
|
|
11
|
+
* ## 🛑 Credit notes are not optional
|
|
12
|
+
*
|
|
13
|
+
* A return listing invoices and no credit notes **does not reconcile**: the
|
|
14
|
+
* output tax declared would be the tax on every sale including the ones that
|
|
15
|
+
* were reversed. So both series are exported, the credit notes as negative
|
|
16
|
+
* amounts, and the file's own total is the net.
|
|
17
|
+
*
|
|
18
|
+
* That is also why the two series exist in the first place — a reversal is its
|
|
19
|
+
* own numbered document, so it has its own row here.
|
|
20
|
+
*
|
|
21
|
+
* ## The rate split comes from `gstByRate`
|
|
22
|
+
*
|
|
23
|
+
* Already on the order (`firestore.ts`), already per rate, already recorded at
|
|
24
|
+
* the moment of supply. Nothing is recomputed from today's tax codes: an order
|
|
25
|
+
* supplied under a rate that has since changed must appear at the rate it was
|
|
26
|
+
* charged, and re-deriving it would silently restate history — the same defect
|
|
27
|
+
* the PDF renderer's own 🛑 records about reading live site settings.
|
|
28
|
+
*/
|
|
29
|
+
export interface Gstr1Row {
|
|
30
|
+
/** `LIR/26-27/0001` or `LIRCN/26-27/0001`. */
|
|
31
|
+
documentNumber: string;
|
|
32
|
+
documentType: "invoice" | "credit_note";
|
|
33
|
+
documentDate: string;
|
|
34
|
+
orderId: string;
|
|
35
|
+
/** The buyer's state, which decides CGST+SGST vs IGST. */
|
|
36
|
+
placeOfSupply: string;
|
|
37
|
+
gstRate: number;
|
|
38
|
+
taxableValue: number;
|
|
39
|
+
cgst: number;
|
|
40
|
+
sgst: number;
|
|
41
|
+
igst: number;
|
|
42
|
+
total: number;
|
|
43
|
+
}
|
|
44
|
+
export interface Gstr1Export {
|
|
45
|
+
rows: Gstr1Row[];
|
|
46
|
+
/** Net of credit notes — what the period actually owes. */
|
|
47
|
+
totals: {
|
|
48
|
+
taxableValue: number;
|
|
49
|
+
cgst: number;
|
|
50
|
+
sgst: number;
|
|
51
|
+
igst: number;
|
|
52
|
+
total: number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Orders in the window that are PAID but carry no invoice number.
|
|
56
|
+
*
|
|
57
|
+
* 🛑 Reported, never silently excluded. A paid order missing from a tax
|
|
58
|
+
* return is the one error this export must not hide, and "the row simply
|
|
59
|
+
* wasn't there" is indistinguishable from "there was nothing to report" in a
|
|
60
|
+
* CSV. Root Cause #84's lesson applied to an accounting artefact: a count
|
|
61
|
+
* that goes quiet is as likely to have stopped looking as to be satisfied.
|
|
62
|
+
*/
|
|
63
|
+
unnumberedPaidOrders: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the export for a date window.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 Windowed on the INVOICE date, not the order date. A COD order placed on
|
|
69
|
+
* 30 March and paid on 2 April is invoiced in the new financial year, and that
|
|
70
|
+
* is the period its tax belongs to — numbering happens at payment for exactly
|
|
71
|
+
* this reason.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildGstr1Export(from: Date, to: Date): Promise<Gstr1Export>;
|
|
74
|
+
export declare function gstr1ToCsv(data: Gstr1Export): string;
|