@mohasinac/appkit 3.2.19 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
  2. package/dist/_internal/server/features/checkout/actions.js +239 -190
  3. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  4. package/dist/_internal/server/functions/scheduled.js +9 -1
  5. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +8 -11
  6. package/dist/_internal/server/jobs/core/emiInstallmentReminder.d.ts +8 -0
  7. package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +78 -0
  8. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.d.ts +2 -2
  9. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -10
  10. package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.d.ts +2 -0
  11. package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.js +2 -0
  12. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  13. package/dist/_internal/server/jobs/handlers/index.js +2 -0
  14. package/dist/_internal/server/providers/index.d.ts +4 -4
  15. package/dist/_internal/server/providers/index.js +4 -4
  16. package/dist/_internal/server/providers/payment/razorpay-mock.d.ts +3 -2
  17. package/dist/_internal/server/providers/payment/razorpay-mock.js +3 -1
  18. package/dist/_internal/server/providers/resolve.d.ts +10 -13
  19. package/dist/_internal/server/providers/resolve.js +9 -15
  20. package/dist/_internal/shared/actions/action-registry.js +28 -0
  21. package/dist/_internal/shared/checkout/rules/_defaults.js +1 -0
  22. package/dist/_internal/shared/checkout/rules/_registry.js +4 -0
  23. package/dist/_internal/shared/checkout/rules/art.rule.d.ts +9 -0
  24. package/dist/_internal/shared/checkout/rules/art.rule.js +5 -0
  25. package/dist/_internal/shared/checkout/rules/auction.rule.js +1 -0
  26. package/dist/_internal/shared/checkout/rules/classified.rule.js +1 -0
  27. package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
  28. package/dist/_internal/shared/checkout/rules/index.js +2 -0
  29. package/dist/_internal/shared/checkout/rules/stickers.rule.d.ts +9 -0
  30. package/dist/_internal/shared/checkout/rules/stickers.rule.js +5 -0
  31. package/dist/_internal/shared/checkout/rules/types.d.ts +6 -0
  32. package/dist/_internal/shared/features/checkout/config.d.ts +16 -1
  33. package/dist/_internal/shared/features/checkout/config.js +16 -1
  34. package/dist/_internal/shared/features/emi/schedule.d.ts +63 -0
  35. package/dist/_internal/shared/features/emi/schedule.js +62 -0
  36. package/dist/_internal/shared/fees/calculator.d.ts +53 -0
  37. package/dist/_internal/shared/fees/calculator.js +40 -0
  38. package/dist/_internal/shared/listing-types/_registry.d.ts +22 -66
  39. package/dist/_internal/shared/listing-types/_registry.js +19 -2
  40. package/dist/_internal/shared/listing-types/art/config.d.ts +15 -0
  41. package/dist/_internal/shared/listing-types/art/config.js +14 -0
  42. package/dist/_internal/shared/listing-types/art/ctas.d.ts +1 -0
  43. package/dist/_internal/shared/listing-types/art/ctas.js +3 -0
  44. package/dist/_internal/shared/listing-types/art/og.d.ts +1 -0
  45. package/dist/_internal/shared/listing-types/art/og.js +1 -0
  46. package/dist/_internal/shared/listing-types/art/schema.d.ts +2 -0
  47. package/dist/_internal/shared/listing-types/art/schema.js +3 -0
  48. package/dist/_internal/shared/listing-types/art/seed-factory.d.ts +1 -0
  49. package/dist/_internal/shared/listing-types/art/seed-factory.js +1 -0
  50. package/dist/_internal/shared/listing-types/auction/config.d.ts +8 -0
  51. package/dist/_internal/shared/listing-types/auction/config.js +6 -0
  52. package/dist/_internal/shared/listing-types/capabilities.js +17 -0
  53. package/dist/_internal/shared/listing-types/classified/config.d.ts +8 -0
  54. package/dist/_internal/shared/listing-types/classified/config.js +6 -0
  55. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +8 -0
  56. package/dist/_internal/shared/listing-types/digital-code/config.js +6 -0
  57. package/dist/_internal/shared/listing-types/live/config.d.ts +8 -0
  58. package/dist/_internal/shared/listing-types/live/config.js +6 -0
  59. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +8 -0
  60. package/dist/_internal/shared/listing-types/pre-order/config.js +6 -0
  61. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +8 -0
  62. package/dist/_internal/shared/listing-types/prize-draw/config.js +6 -0
  63. package/dist/_internal/shared/listing-types/standard/config.d.ts +8 -0
  64. package/dist/_internal/shared/listing-types/standard/config.js +6 -0
  65. package/dist/_internal/shared/listing-types/stickers/config.d.ts +15 -0
  66. package/dist/_internal/shared/listing-types/stickers/config.js +14 -0
  67. package/dist/_internal/shared/listing-types/stickers/ctas.d.ts +1 -0
  68. package/dist/_internal/shared/listing-types/stickers/ctas.js +3 -0
  69. package/dist/_internal/shared/listing-types/stickers/og.d.ts +1 -0
  70. package/dist/_internal/shared/listing-types/stickers/og.js +1 -0
  71. package/dist/_internal/shared/listing-types/stickers/schema.d.ts +2 -0
  72. package/dist/_internal/shared/listing-types/stickers/schema.js +3 -0
  73. package/dist/_internal/shared/listing-types/stickers/seed-factory.d.ts +1 -0
  74. package/dist/_internal/shared/listing-types/stickers/seed-factory.js +1 -0
  75. package/dist/client.d.ts +7 -2
  76. package/dist/client.js +4 -2
  77. package/dist/constants/field-names.d.ts +2 -6
  78. package/dist/constants/field-names.js +2 -6
  79. package/dist/contracts/index.d.ts +4 -2
  80. package/dist/contracts/index.js +6 -0
  81. package/dist/contracts/payment.d.ts +14 -9
  82. package/dist/contracts/payment.js +11 -1
  83. package/dist/contracts/shipping.d.ts +14 -9
  84. package/dist/contracts/shipping.js +11 -1
  85. package/dist/features/about/components/PublicProfileView.js +3 -6
  86. package/dist/features/account/components/UserOffersPanel.js +0 -1
  87. package/dist/features/admin/components/AdminAddressBookView.d.ts +11 -0
  88. package/dist/features/admin/components/AdminAddressBookView.js +30 -0
  89. package/dist/features/admin/components/AdminArtView.d.ts +3 -0
  90. package/dist/features/admin/components/AdminArtView.js +45 -0
  91. package/dist/features/admin/components/AdminCarouselEditorView.js +2 -3
  92. package/dist/features/admin/components/AdminFulfillmentView.js +0 -1
  93. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  94. package/dist/features/admin/components/AdminSiteSettingsView.js +14 -9
  95. package/dist/features/admin/components/AdminStickersView.d.ts +3 -0
  96. package/dist/features/admin/components/AdminStickersView.js +45 -0
  97. package/dist/features/admin/components/index.d.ts +6 -0
  98. package/dist/features/admin/components/index.js +4 -0
  99. package/dist/features/admin/hooks/useChat.js +0 -1
  100. package/dist/features/admin/schemas/firestore.d.ts +19 -11
  101. package/dist/features/admin/schemas/firestore.js +12 -2
  102. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +3 -2
  103. package/dist/features/auth/hooks/useAuth.js +35 -10
  104. package/dist/features/auth/schemas/firestore.d.ts +2 -6
  105. package/dist/features/cart/schemas/firestore.d.ts +3 -2
  106. package/dist/features/consultation/schemas/index.d.ts +2 -2
  107. package/dist/features/history/hooks/useHistory.js +0 -1
  108. package/dist/features/loyalty/schemas/index.d.ts +6 -6
  109. package/dist/features/messages/hooks/useConversation.js +0 -1
  110. package/dist/features/messages/hooks/useConversations.js +0 -1
  111. package/dist/features/orders/repository/orders.repository.d.ts +20 -2
  112. package/dist/features/orders/repository/orders.repository.js +34 -5
  113. package/dist/features/orders/schemas/firestore.d.ts +33 -7
  114. package/dist/features/orders/schemas/firestore.js +1 -1
  115. package/dist/features/orders/schemas/index.d.ts +176 -31
  116. package/dist/features/orders/schemas/index.js +20 -6
  117. package/dist/features/orders/types/index.d.ts +3 -3
  118. package/dist/features/payments/repository/payout.repository.js +2 -2
  119. package/dist/features/payments/schemas/firestore.d.ts +3 -3
  120. package/dist/features/payments/schemas/firestore.js +2 -2
  121. package/dist/features/payments/schemas/index.d.ts +5 -5
  122. package/dist/features/payments/schemas/index.js +2 -2
  123. package/dist/features/products/api/[id]/route.js +4 -1
  124. package/dist/features/products/api/route.js +4 -1
  125. package/dist/features/products/components/CompareOverlay.js +7 -9
  126. package/dist/features/products/components/ProductDetailPageView.d.ts +3 -1
  127. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  128. package/dist/features/products/components/ProductForm.js +18 -7
  129. package/dist/features/products/components/ProductGrid.js +7 -12
  130. package/dist/features/products/components/ShowGroupSection.js +3 -5
  131. package/dist/features/products/components/SublistingCarouselSection.js +3 -6
  132. package/dist/features/products/constants/listing-tabs.d.ts +32 -0
  133. package/dist/features/products/constants/listing-tabs.js +8 -0
  134. package/dist/features/products/index.d.ts +1 -1
  135. package/dist/features/products/index.js +3 -1
  136. package/dist/features/products/schemas/firestore.d.ts +20 -1
  137. package/dist/features/products/schemas/firestore.js +2 -0
  138. package/dist/features/products/schemas/index.d.ts +48 -0
  139. package/dist/features/products/schemas/index.js +9 -0
  140. package/dist/features/products/schemas/product-features.d.ts +1 -1
  141. package/dist/features/products/types/index.d.ts +5 -3
  142. package/dist/features/products/utils/listing-type.d.ts +6 -0
  143. package/dist/features/products/utils/listing-type.js +3 -0
  144. package/dist/features/reviews/api/[id]/route.js +5 -4
  145. package/dist/features/seller/actions/seller-actions.d.ts +29 -14
  146. package/dist/features/seller/actions/seller-actions.js +101 -27
  147. package/dist/features/seller/components/FulfillmentView.js +0 -1
  148. package/dist/features/seller/components/SellerAddressesView.js +0 -1
  149. package/dist/features/seller/components/SellerArtView.d.ts +7 -0
  150. package/dist/features/seller/components/SellerArtView.js +114 -0
  151. package/dist/features/seller/components/SellerOffersPanel.js +0 -1
  152. package/dist/features/seller/components/SellerOrdersView.js +38 -1
  153. package/dist/features/seller/components/SellerProductShell.d.ts +5 -1
  154. package/dist/features/seller/components/SellerProductShell.js +69 -59
  155. package/dist/features/seller/components/SellerReviewsView.js +0 -1
  156. package/dist/features/seller/components/SellerShippingView.js +6 -25
  157. package/dist/features/seller/components/SellerStickersView.d.ts +7 -0
  158. package/dist/features/seller/components/SellerStickersView.js +114 -0
  159. package/dist/features/seller/components/index.d.ts +4 -0
  160. package/dist/features/seller/components/index.js +2 -0
  161. package/dist/features/seller/messages/en.json +1 -1
  162. package/dist/features/seller/schemas/index.d.ts +7 -7
  163. package/dist/features/seller/schemas/index.js +1 -1
  164. package/dist/features/seller/types/index.d.ts +1 -1
  165. package/dist/features/stores/components/StoreDetailLayoutView.js +18 -2
  166. package/dist/features/stores/schemas/firestore.d.ts +7 -1
  167. package/dist/features/stores/schemas/index.d.ts +19 -16
  168. package/dist/features/stores/schemas/index.js +1 -1
  169. package/dist/index.d.ts +21 -36
  170. package/dist/index.js +27 -39
  171. package/dist/next/routing/route-map.d.ts +22 -0
  172. package/dist/next/routing/route-map.js +10 -0
  173. package/dist/providers/payment-manual/index.d.ts +40 -0
  174. package/dist/providers/payment-manual/index.js +91 -0
  175. package/dist/providers/payment-razorpay/index.d.ts +4 -2
  176. package/dist/providers/payment-razorpay/index.js +4 -1
  177. package/dist/providers/shipping-manual/index.d.ts +22 -0
  178. package/dist/providers/shipping-manual/index.js +47 -0
  179. package/dist/providers.d.ts +2 -2
  180. package/dist/providers.js +5 -3
  181. package/dist/schemas/registry.d.ts +129 -70
  182. package/dist/schemas/registry.js +0 -4
  183. package/dist/security/pii-encrypt.js +0 -7
  184. package/dist/security/pii-redact.js +0 -1
  185. package/dist/seed/conversations-seed-data.js +2 -2
  186. package/dist/seed/faq-seed-data.js +7 -7
  187. package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
  188. package/dist/seed/index.d.ts +2 -0
  189. package/dist/seed/index.js +2 -0
  190. package/dist/seed/manifest.js +20 -16
  191. package/dist/seed/payouts-seed-data.js +5 -5
  192. package/dist/seed/products-art-seed-data.d.ts +11 -0
  193. package/dist/seed/products-art-seed-data.js +125 -0
  194. package/dist/seed/products-preorders-seed-data.d.ts +2 -0
  195. package/dist/seed/products-stickers-seed-data.d.ts +11 -0
  196. package/dist/seed/products-stickers-seed-data.js +125 -0
  197. package/dist/seed/site-settings-seed-data.js +13 -4
  198. package/dist/seed/stores-seed-data.js +11 -11
  199. package/dist/server.d.ts +7 -15
  200. package/dist/server.js +17 -42
  201. package/dist/ui/rich-text/RichText.js +1 -1
  202. package/package.json +1 -1
@@ -20,7 +20,7 @@ export declare const MAX_STORE_CUSTOM_FEATURES = 20;
20
20
  export declare const MAX_FEATURES_PER_PRODUCT = 10;
21
21
  export type ProductFeatureScope = "platform" | "store";
22
22
  export type ProductFeatureCategory = "shipping" | "seller" | "condition" | "platform" | "auction" | "preorder" | "custom";
23
- export type ProductFeatureProductType = "product" | "auction" | "preorder" | "prize-draw" | "classified" | "digital-code" | "live" | "all";
23
+ export type ProductFeatureProductType = "product" | "auction" | "preorder" | "prize-draw" | "classified" | "digital-code" | "live" | "art" | "stickers" | "all";
24
24
  /**
25
25
  * Either an appkit icon-set name key (e.g. "truck") or a raw SVG path-d string.
26
26
  * Path strings must start with "M " — see isFeatureIconPath().
@@ -1,6 +1,6 @@
1
1
  import type { MediaField } from "../../media/types/index";
2
- import type { CustomField, CustomSection } from "../schemas/firestore";
3
- export type { CustomField, CustomSection } from "../schemas/firestore";
2
+ import type { CustomField, CustomSection, ProductPrintMeta } from "../schemas/firestore";
3
+ export type { CustomField, CustomSection, ProductPrintMeta } from "../schemas/firestore";
4
4
  export type ProductStatus = "draft" | "published" | "in_review" | "archived";
5
5
  export type ProductCondition = "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded";
6
6
  /**
@@ -10,7 +10,7 @@ export type ProductCondition = "new" | "like_new" | "good" | "fair" | "poor" | "
10
10
  * accepts legacy alias tokens (`preorder` → `pre-order`, `product` → `standard`)
11
11
  * but stored values are always one of these five.
12
12
  */
13
- export type ListingType = "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live";
13
+ export type ListingType = "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live" | "art" | "stickers";
14
14
  export interface ProductImage {
15
15
  url: string;
16
16
  thumbnailUrl?: string;
@@ -39,6 +39,8 @@ export interface ProductItem {
39
39
  };
40
40
  featured?: boolean;
41
41
  isPromoted?: boolean;
42
+ allowShipBeforeEmiComplete?: boolean;
43
+ printMeta?: ProductPrintMeta;
42
44
  currentBid?: number;
43
45
  availableQuantity?: number;
44
46
  /** @deprecated Use categorySlugs[0] */
@@ -33,3 +33,9 @@ export declare const isDigitalCodeListing: (input?: {
33
33
  export declare const isLiveListing: (input?: {
34
34
  listingType?: ListingType;
35
35
  }) => boolean;
36
+ export declare const isArtListing: (input?: {
37
+ listingType?: ListingType;
38
+ }) => boolean;
39
+ export declare const isStickersListing: (input?: {
40
+ listingType?: ListingType;
41
+ }) => boolean;
@@ -19,6 +19,9 @@ export const isPrizeDrawListing = (input) => normalizeListingType(input) === "pr
19
19
  export const isClassifiedListing = (input) => normalizeListingType(input) === "classified";
20
20
  export const isDigitalCodeListing = (input) => normalizeListingType(input) === "digital-code";
21
21
  export const isLiveListing = (input) => normalizeListingType(input) === "live";
22
+ // Art/stickers session — printed-only physical-goods listing types.
23
+ export const isArtListing = (input) => normalizeListingType(input) === "art";
24
+ export const isStickersListing = (input) => normalizeListingType(input) === "stickers";
22
25
  // SB-UNI-D — isBundleListing removed. Bundles are now a categoryType
23
26
  // discriminator on CategoryDocument; consumers should query
24
27
  // `categoriesRepository.findBySlugAndType(slug, "bundle")` instead.
@@ -13,6 +13,7 @@ import { z } from "zod";
13
13
  import { getProviders } from "../../../../contracts";
14
14
  import { createRouteHandler } from "../../../../next";
15
15
  import { normalizeError } from "../../../../errors/normalize";
16
+ import { isAdminUser, isModeratorUser } from "../../../auth/role-predicates";
16
17
  const ERR_REVIEW_NOT_FOUND = "Review not found";
17
18
  // ---- GET /api/reviews/[id] ---------------------------------------------------
18
19
  export async function GET(_request, context) {
@@ -70,8 +71,8 @@ export const reviewItemPATCH = createRouteHandler({
70
71
  if (!review)
71
72
  return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
72
73
  const isOwner = review.userId === user.uid;
73
- const isModerator = user.role === "moderator";
74
- const isAdmin = user.role === "admin";
74
+ const isModerator = isModeratorUser(user);
75
+ const isAdmin = isAdminUser(user);
75
76
  if (!isOwner && !isModerator && !isAdmin) {
76
77
  return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
77
78
  }
@@ -101,8 +102,8 @@ export const reviewItemDELETE = createRouteHandler({
101
102
  if (!review)
102
103
  return NextResponse.json({ success: false, error: ERR_REVIEW_NOT_FOUND }, { status: 404 });
103
104
  const isOwner = review.userId === user.uid;
104
- const isModerator = user.role === "moderator";
105
- const isAdmin = user.role === "admin";
105
+ const isModerator = isModeratorUser(user);
106
+ const isAdmin = isAdminUser(user);
106
107
  if (!isOwner && !isModerator && !isAdmin) {
107
108
  return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
108
109
  }
@@ -5,13 +5,11 @@
5
5
  * explicit params. Called by letitrip thin wrappers that handle auth,
6
6
  * rate-limiting, and user-facing validation.
7
7
  *
8
- * Note: Shiprocket-specific actions (updateSellerShipping,
9
- * verifyShiprocketPickupOtp, shiprocketShipOrder) remain in the letitrip
10
- * thin wrapper because they use the @/lib/shiprocket/client SDK which is
11
- * a permanent letitrip-only dependency.
8
+ * Note: updateSellerShipping remains in the letitrip thin wrapper
9
+ * (src/actions/seller.actions.ts) alongside the other seller server actions.
12
10
  */
13
11
  import type { JsonValue } from "../../../schemas/types";
14
- import type { SellerPayoutDetails } from "../../auth/schemas/firestore";
12
+ import type { SellerPayoutDetails, SellerShippingConfig } from "../../auth/schemas/firestore";
15
13
  import type { StoreDocument } from "../../stores/schemas/firestore";
16
14
  import type { ProductDocument } from "../../products/schemas/firestore";
17
15
  import type { OrderDocument } from "../../orders/schemas/firestore";
@@ -86,6 +84,14 @@ export interface CustomShipOrderInput {
86
84
  trackingNumber: string;
87
85
  trackingUrl: string;
88
86
  }
87
+ export interface MarkEmiInstallmentPaidInput {
88
+ /** 1-based installment number, matches EmiInstallment.index. */
89
+ installmentIndex: number;
90
+ /** Buyer-supplied UTR/reference number for this installment's manual payment. */
91
+ transactionId?: string;
92
+ /** Signed-URL media slug for the buyer's payment proof for this installment. */
93
+ proofUrl?: string;
94
+ }
89
95
  export interface SellerListParams {
90
96
  filters?: string;
91
97
  sorts?: string;
@@ -109,15 +115,7 @@ export declare function requestPayout(userId: string, userName: string, userEmai
109
115
  export declare function bulkSellerOrder(userId: string, userRole: string, userDisplayName: string, userEmail: string, orderIds: string[]): Promise<BulkSellerOrderResult>;
110
116
  export declare function createSellerProduct(userId: string, userName: string, userEmail: string, input: Record<string, JsonValue>): Promise<void>;
111
117
  export declare function getSellerStore(userId: string): Promise<StoreDocument | null>;
112
- export declare function getSellerShipping(userId: string): Promise<{
113
- method: import("../../auth").SellerShippingMethod;
114
- customShippingPrice?: number;
115
- customCarrierName?: string;
116
- shiprocketEmail?: string;
117
- shiprocketTokenExpiry?: Date;
118
- pickupAddress?: import("../../auth").SellerPickupAddress;
119
- isConfigured: boolean;
120
- } | null>;
118
+ export declare function getSellerShipping(userId: string): Promise<SellerShippingConfig | null>;
121
119
  export declare function getSellerPayoutSettings(userId: string): Promise<SellerPayoutDetails | {
122
120
  method: string;
123
121
  isConfigured: boolean;
@@ -166,8 +164,25 @@ export declare function listSellerMyProducts(userId: string, params?: SellerList
166
164
  }>;
167
165
  export declare function sellerUpdateProduct(userId: string, userRole: string, productId: string, input: Record<string, JsonValue>): Promise<ProductDocument>;
168
166
  export declare function sellerDeleteProduct(userId: string, userRole: string, productId: string): Promise<void>;
167
+ /**
168
+ * EMI orders stay unshipped until every installment is paid, UNLESS every
169
+ * product in the order has its `allowShipBeforeEmiComplete` flag set. This
170
+ * is the single choke point for the "ship" transition, so it can't be
171
+ * bypassed by a different route/action reaching `orderRepository.update`
172
+ * directly with `status: "shipped"`.
173
+ */
174
+ export declare function assertEmiShippable(order: OrderDocument): Promise<void>;
169
175
  export declare function customShipOrder(userId: string, userRole: string, orderId: string, input: CustomShipOrderInput): Promise<{
170
176
  orderId: string;
171
177
  method: string;
172
178
  }>;
179
+ /**
180
+ * Records collection of one EMI installment. Seller/admin marks the
181
+ * installment paid after verifying the buyer's manual transfer (UTR +
182
+ * proof). Recomputes `emiRemainingBalance` from the still-pending
183
+ * installments and flips `emiComplete` once none remain — that flag is the
184
+ * single gate `assertEmiShippable` reads, so this is the only place an
185
+ * EMI order can become shippable.
186
+ */
187
+ export declare function markEmiInstallmentPaid(userId: string, userRole: string, orderId: string, input: MarkEmiInstallmentPaidInput): Promise<OrderDocument>;
173
188
  export {};
@@ -5,10 +5,8 @@
5
5
  * explicit params. Called by letitrip thin wrappers that handle auth,
6
6
  * rate-limiting, and user-facing validation.
7
7
  *
8
- * Note: Shiprocket-specific actions (updateSellerShipping,
9
- * verifyShiprocketPickupOtp, shiprocketShipOrder) remain in the letitrip
10
- * thin wrapper because they use the @/lib/shiprocket/client SDK which is
11
- * a permanent letitrip-only dependency.
8
+ * Note: updateSellerShipping remains in the letitrip thin wrapper
9
+ * (src/actions/seller.actions.ts) alongside the other seller server actions.
12
10
  */
13
11
  import { serverLogger } from "../../../monitoring";
14
12
  import { isAdminUser, isSellerUser } from "../../auth/role-predicates";
@@ -23,7 +21,8 @@ import { payoutRepository } from "../../payments/repository/payout.repository";
23
21
  import { PAYOUT_FIELDS } from "../../payments/schemas";
24
22
  import { couponsRepository } from "../../promotions/repository/coupons.repository";
25
23
  import { generateStoreSlug } from "../../stores/schemas/firestore";
26
- import { DEFAULT_PLATFORM_FEE_RATE } from "../../payments/schemas/firestore";
24
+ import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
25
+ import { computePayoutDeduction } from "../../../_internal/shared/fees/calculator";
27
26
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
28
27
  import { finalizeStagedMediaUrl, finalizeStagedMediaField, finalizeStagedMediaArray, } from "../../media/finalize";
29
28
  // --- Helper -------------------------------------------------------------------
@@ -169,14 +168,17 @@ async function computeSellerEarnings(sellerId) {
169
168
  const paidOutIds = await payoutRepository.getPaidOutOrderIds(sellerId);
170
169
  const eligibleOrders = deliveredOrders.filter((o) => !paidOutIds.has(o.id));
171
170
  const grossAmount = eligibleOrders.reduce((sum, o) => sum + (o.totalPrice ?? 0), 0);
172
- const platformFee = parseFloat((grossAmount * DEFAULT_PLATFORM_FEE_RATE).toFixed(2));
173
- const netAmount = parseFloat((grossAmount - platformFee).toFixed(2));
171
+ const siteSettings = await siteSettingsRepository.getSingleton();
172
+ const deduction = computePayoutDeduction(grossAmount, siteSettings.commissions);
174
173
  return {
175
174
  eligibleOrders,
176
175
  grossAmount,
177
- platformFee,
178
- netAmount,
176
+ platformFee: deduction.platformFee,
177
+ gatewayFee: deduction.gatewayFee,
178
+ gstAmount: deduction.gstOnFee,
179
+ netAmount: deduction.netAmount,
179
180
  productCount: products.length,
181
+ commissions: siteSettings.commissions,
180
182
  };
181
183
  }
182
184
  export async function requestPayout(userId, userName, userEmail, input) {
@@ -195,7 +197,11 @@ export async function requestPayout(userId, userName, userEmail, input) {
195
197
  amount: earnings.netAmount,
196
198
  grossAmount: earnings.grossAmount,
197
199
  platformFee: earnings.platformFee,
198
- platformFeeRate: DEFAULT_PLATFORM_FEE_RATE,
200
+ platformFeeRate: earnings.commissions.platformFeePercent / 100,
201
+ gatewayFee: earnings.gatewayFee,
202
+ gatewayFeeRate: (earnings.commissions.gatewayFeePercent ?? 0) / 100,
203
+ gstAmount: earnings.gstAmount,
204
+ gstRate: earnings.commissions.gstPercent / 100,
199
205
  currency: getDefaultCurrency(),
200
206
  paymentMethod: input.paymentMethod,
201
207
  ...(input.bankAccount && { bankAccount: input.bankAccount }),
@@ -245,23 +251,32 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
245
251
  skipped.push(id);
246
252
  continue;
247
253
  }
254
+ if (order.emiEnabled && !order.emiComplete) {
255
+ // Delivered via the seller's allowShipBeforeEmiComplete override, but the
256
+ // buyer hasn't finished paying — hold the payout until emiComplete.
257
+ skipped.push(id);
258
+ continue;
259
+ }
248
260
  eligible.push(order);
249
261
  }
250
262
  if (eligible.length === 0)
251
263
  throw new ValidationError("No eligible orders found.");
252
- const PLATFORM_COMMISSION_RATE = DEFAULT_PLATFORM_FEE_RATE;
253
264
  const grossAmount = eligible.reduce((sum, o) => sum + (o.totalPrice ?? 0), 0);
254
- const platformFee = Math.round(grossAmount * PLATFORM_COMMISSION_RATE * 100) / 100;
255
- const netAmount = Math.round((grossAmount - platformFee) * 100) / 100;
265
+ const bulkSiteSettings = await siteSettingsRepository.getSingleton();
266
+ const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions);
256
267
  const payoutDoc = await payoutRepository.create({
257
268
  storeId: userId,
258
269
  sellerName: userDisplayName,
259
270
  sellerEmail: userEmail,
260
271
  orderIds: eligible.map((o) => o.id),
261
- amount: netAmount,
272
+ amount: bulkDeduction.netAmount,
262
273
  grossAmount,
263
- platformFee,
264
- platformFeeRate: PLATFORM_COMMISSION_RATE,
274
+ platformFee: bulkDeduction.platformFee,
275
+ platformFeeRate: bulkSiteSettings.commissions.platformFeePercent / 100,
276
+ gatewayFee: bulkDeduction.gatewayFee,
277
+ gatewayFeeRate: (bulkSiteSettings.commissions.gatewayFeePercent ?? 0) / 100,
278
+ gstAmount: bulkDeduction.gstOnFee,
279
+ gstRate: bulkSiteSettings.commissions.gstPercent / 100,
265
280
  currency: getDefaultCurrency(),
266
281
  paymentMethod: userDoc.payoutDetails.method === "upi"
267
282
  ? "upi"
@@ -281,7 +296,7 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
281
296
  userId,
282
297
  payoutId,
283
298
  orderCount: eligible.length,
284
- netAmount,
299
+ netAmount: bulkDeduction.netAmount,
285
300
  });
286
301
  return {
287
302
  payoutId,
@@ -289,9 +304,9 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
289
304
  skipped,
290
305
  eligibleCount: eligible.length,
291
306
  skippedCount: skipped.length,
292
- netAmount,
307
+ netAmount: bulkDeduction.netAmount,
293
308
  grossAmount,
294
- platformFee,
309
+ platformFee: bulkDeduction.platformFee,
295
310
  };
296
311
  }
297
312
  // --- Create Seller Product ----------------------------------------------------
@@ -314,14 +329,7 @@ export async function getSellerShipping(userId) {
314
329
  const userData = await userRepository.findById(userId);
315
330
  if (!userData)
316
331
  return null;
317
- const config = userData.shippingConfig;
318
- if (!config)
319
- return null;
320
- if (config.method === "shiprocket" && config.shiprocketToken) {
321
- const { shiprocketToken: _removed, ...safe } = config;
322
- return safe;
323
- }
324
- return config;
332
+ return userData.shippingConfig ?? null;
325
333
  }
326
334
  export async function getSellerPayoutSettings(userId) {
327
335
  const userData = await userRepository.findById(userId);
@@ -458,6 +466,23 @@ export async function sellerDeleteProduct(userId, userRole, productId) {
458
466
  serverLogger.info("sellerDeleteProduct", { userId, productId });
459
467
  }
460
468
  // --- Custom Ship Order --------------------------------------------------------
469
+ /**
470
+ * EMI orders stay unshipped until every installment is paid, UNLESS every
471
+ * product in the order has its `allowShipBeforeEmiComplete` flag set. This
472
+ * is the single choke point for the "ship" transition, so it can't be
473
+ * bypassed by a different route/action reaching `orderRepository.update`
474
+ * directly with `status: "shipped"`.
475
+ */
476
+ export async function assertEmiShippable(order) {
477
+ if (!order.emiEnabled || order.emiComplete)
478
+ return;
479
+ const productIds = (order.items ?? []).map((i) => i.productId).filter(Boolean);
480
+ const products = await Promise.all(productIds.map((id) => productRepository.findById(id)));
481
+ const allAllowEarlyShip = products.length > 0 && products.every((p) => p?.allowShipBeforeEmiComplete === true);
482
+ if (!allAllowEarlyShip) {
483
+ throw new ValidationError("This order is still on an EMI plan — it can't ship until all installments are paid, unless the seller has enabled early shipping for every item in the order.");
484
+ }
485
+ }
461
486
  export async function customShipOrder(userId, userRole, orderId, input) {
462
487
  const order = await orderRepository.findById(orderId);
463
488
  if (!order)
@@ -469,6 +494,7 @@ export async function customShipOrder(userId, userRole, orderId, input) {
469
494
  throw new ValidationError("Order is already shipped");
470
495
  if (order.status !== OrderStatusValues.CONFIRMED)
471
496
  throw new ValidationError("Order must be confirmed before shipping");
497
+ await assertEmiShippable(order);
472
498
  await orderRepository.update(orderId, {
473
499
  status: OrderStatusValues.SHIPPED,
474
500
  shippingMethod: "custom",
@@ -485,3 +511,51 @@ export async function customShipOrder(userId, userRole, orderId, input) {
485
511
  });
486
512
  return { orderId, method: "custom" };
487
513
  }
514
+ /**
515
+ * Records collection of one EMI installment. Seller/admin marks the
516
+ * installment paid after verifying the buyer's manual transfer (UTR +
517
+ * proof). Recomputes `emiRemainingBalance` from the still-pending
518
+ * installments and flips `emiComplete` once none remain — that flag is the
519
+ * single gate `assertEmiShippable` reads, so this is the only place an
520
+ * EMI order can become shippable.
521
+ */
522
+ export async function markEmiInstallmentPaid(userId, userRole, orderId, input) {
523
+ const order = await orderRepository.findById(orderId);
524
+ if (!order)
525
+ throw new NotFoundError("Order not found");
526
+ if (userRole !== "admin" && order.storeId !== userId)
527
+ throw new AuthorizationError("You do not own this order");
528
+ if (!order.emiEnabled)
529
+ throw new ValidationError("This order is not on an EMI plan");
530
+ const installments = order.emiInstallments ?? [];
531
+ const target = installments.find((i) => i.index === input.installmentIndex);
532
+ if (!target)
533
+ throw new ValidationError(`Installment ${input.installmentIndex} not found on this order`);
534
+ if (target.status === "paid")
535
+ throw new ValidationError("This installment is already marked paid");
536
+ const updatedInstallments = installments.map((i) => i.index === input.installmentIndex
537
+ ? {
538
+ ...i,
539
+ status: "paid",
540
+ paidAt: new Date(),
541
+ transactionId: input.transactionId,
542
+ proofUrl: input.proofUrl,
543
+ }
544
+ : i);
545
+ const emiRemainingBalance = updatedInstallments
546
+ .filter((i) => i.status !== "paid")
547
+ .reduce((sum, i) => sum + i.amount, 0);
548
+ const emiComplete = updatedInstallments.every((i) => i.status === "paid");
549
+ const updated = await orderRepository.update(orderId, {
550
+ emiInstallments: updatedInstallments,
551
+ emiRemainingBalance,
552
+ emiComplete,
553
+ });
554
+ serverLogger.info("markEmiInstallmentPaid", {
555
+ orderId,
556
+ userId,
557
+ installmentIndex: input.installmentIndex,
558
+ emiComplete,
559
+ });
560
+ return updated;
561
+ }
@@ -31,7 +31,6 @@ export function FulfillmentView({ currentUserId, }) {
31
31
  const [actionLoadingId, setActionLoadingId] = useState(null);
32
32
  const { showToast } = useToast();
33
33
  const fetchOrders = useCallback(async () => {
34
- // toast-intentionally-silent: background data loader — normalizeError logs, no user-facing toast needed
35
34
  setIsLoadingOrders(true);
36
35
  try {
37
36
  const res = await apiClient.get(SELLER_ENDPOINTS.ORDERS_FULFILLMENT);
@@ -68,7 +68,6 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
68
68
  const [saveError, setSaveError] = useState(null);
69
69
  const [deleteTargetAddr, setDeleteTargetAddr] = useState(null);
70
70
  const load = useCallback(async () => {
71
- // toast-intentionally-silent: background address loader — error displayed in inline errorMessage state
72
71
  setIsLoading(true);
73
72
  setErrorMessage(null);
74
73
  try {
@@ -0,0 +1,7 @@
1
+ export interface SellerArtViewProps {
2
+ onCreateClick?: () => void;
3
+ onEditClick?: (id: string) => void;
4
+ onDelete?: (id: string) => Promise<void>;
5
+ onBulkDelete?: (ids: string[]) => Promise<void>;
6
+ }
7
+ export declare function SellerArtView({ onCreateClick, onEditClick, onDelete, onBulkDelete, }: SellerArtViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * SellerArtView — seller browse/manage of art print listings
5
+ * (EMI/art-stickers session). Mirrors SellerClassifiedView's pattern.
6
+ */
7
+ import { Badge, sortBy } from "@mohasinac/appkit";
8
+ import { useState, useCallback } from "react";
9
+ import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
10
+ import { ConfirmDeleteModal, RowActionMenu, Text } from "../../../ui";
11
+ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
12
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
13
+ import { ROUTES } from "../../..";
14
+ import { toRecordArray, toRelativeDate, toRupees, toStringValue, } from "../../admin/hooks/useAdminListingData";
15
+ import { DataListingView } from "../../admin/components/DataListingView";
16
+ const COLUMNS = [
17
+ { key: "title", header: "Listing", render: (row) => _jsx(Text, { size: "sm", weight: "medium", children: row.title }) },
18
+ { key: "price", header: "Price", render: (row) => _jsx(Text, { className: "tabular-nums", size: "sm", children: row.price }) },
19
+ {
20
+ key: "size",
21
+ header: "Size",
22
+ render: (row) => _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: row.size || "—" }),
23
+ },
24
+ {
25
+ key: "status",
26
+ header: "Status",
27
+ render: (row) => (_jsx(Badge, { variant: row.status === "active" ? "active" : "inactive", size: "xs", className: "capitalize", children: row.status })),
28
+ },
29
+ {
30
+ key: "createdAt",
31
+ header: "Created",
32
+ render: (row) => _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: row.createdAt }),
33
+ },
34
+ ];
35
+ export function SellerArtView({ onCreateClick, onEditClick, onDelete, onBulkDelete, }) {
36
+ const [deleteTargetId, setDeleteTargetId] = useState(null);
37
+ const { deletingId, handleDelete: performDelete } = useEntityDelete({
38
+ endpoint: (id) => `/api/store/products/${id}`,
39
+ deleteFn: onDelete,
40
+ successMessage: "Listing deleted.",
41
+ fetchOptions: { credentials: "include" },
42
+ });
43
+ const handleDelete = useCallback(async (id) => {
44
+ await performDelete(id);
45
+ setDeleteTargetId(null);
46
+ }, [performDelete]);
47
+ const handleEdit = useCallback((id) => {
48
+ if (onEditClick)
49
+ onEditClick(id);
50
+ else
51
+ window.location.href = String(ROUTES.STORE.ART_EDIT(id));
52
+ }, [onEditClick]);
53
+ const handleCreate = useCallback(() => {
54
+ if (onCreateClick)
55
+ onCreateClick();
56
+ else
57
+ window.location.href = String(ROUTES.STORE.ART_NEW);
58
+ }, [onCreateClick]);
59
+ const config = {
60
+ portal: "seller",
61
+ title: "Art",
62
+ searchPlaceholder: "Search art prints...",
63
+ emptyLabel: "No art listings yet — list your first print",
64
+ filterKeys: [],
65
+ defaultSort: sortBy("createdAt", "DESC"),
66
+ queryKey: ["seller", "art"],
67
+ endpoint: SELLER_ENDPOINTS.PRODUCTS,
68
+ sortOptions: [
69
+ { value: sortBy("createdAt", "DESC"), label: "Newest" },
70
+ { value: sortBy("createdAt", "ASC"), label: "Oldest" },
71
+ { value: "productTitle", label: "Name A–Z" },
72
+ { value: sortBy("price", "ASC"), label: "Price: Low–High" },
73
+ { value: sortBy("price", "DESC"), label: "Price: High–Low" },
74
+ ],
75
+ columns: COLUMNS,
76
+ mapRows: (response) => toRecordArray(response.products).map((item, index) => {
77
+ const printMeta = (item.printMeta ?? {});
78
+ return {
79
+ id: toStringValue(item.id, `art-${index}`),
80
+ title: toStringValue(item.productTitle ?? item.title, "Untitled"),
81
+ price: toRupees(item.price),
82
+ size: toStringValue(printMeta.size, ""),
83
+ status: toStringValue(item.status, "draft"),
84
+ createdAt: toRelativeDate(item.createdAt),
85
+ };
86
+ }),
87
+ getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
88
+ buildFilters: () => "listingType==art",
89
+ primaryAction: { label: "New Art Print", onClick: () => handleCreate() },
90
+ buildBulkActions: onBulkDelete
91
+ ? (selection) => [
92
+ {
93
+ id: "bulk-delete",
94
+ label: ACTIONS.STORE["delete-listing"].label,
95
+ variant: "danger",
96
+ onClick: async () => {
97
+ await onBulkDelete(selection.selectedIds);
98
+ selection.clearSelection();
99
+ },
100
+ },
101
+ ]
102
+ : undefined,
103
+ renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
104
+ { label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
105
+ {
106
+ label: ACTIONS.STORE["delete-listing"].label,
107
+ destructive: true,
108
+ onClick: () => setDeleteTargetId(row.id),
109
+ disabled: deletingId === row.id,
110
+ },
111
+ ] })),
112
+ };
113
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Art Listing", message: "Are you sure you want to delete this art listing? This cannot be undone.", onConfirm: () => handleDelete(deleteTargetId), onClose: () => setDeleteTargetId(null), isDeleting: deletingId === deleteTargetId }))] }));
114
+ }
@@ -117,7 +117,6 @@ export function SellerOffersPanel({ fetchEndpoint = "/api/store/offers", onRespo
117
117
  const [showLoginModal, setShowLoginModal] = useState(false);
118
118
  const [statusFilter, setStatusFilter] = useState("all");
119
119
  const loadOffers = useCallback(async () => {
120
- // toast-intentionally-silent: background data loader — error displayed in inline fetchError state
121
120
  setLoading(true);
122
121
  setFetchError("");
123
122
  try {
@@ -48,6 +48,11 @@ const UPDATE_STATUS_OPTIONS = [
48
48
  { value: "delivered", label: "Delivered" },
49
49
  { value: "cancelled", label: "Cancelled" },
50
50
  ];
51
+ const EMI_INSTALLMENT_BADGE_VARIANT = {
52
+ paid: "success",
53
+ pending: "warning",
54
+ overdue: "danger",
55
+ };
51
56
  // ---------------------------------------------------------------------------
52
57
  // Order Detail Drawer
53
58
  // ---------------------------------------------------------------------------
@@ -61,6 +66,9 @@ function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
61
66
  const [trackingNumber, setTrackingNumber] = useState("");
62
67
  const [carrier, setCarrier] = useState("");
63
68
  const [trackingUrl, setTrackingUrl] = useState("");
69
+ const [markingPaidIndex, setMarkingPaidIndex] = useState(null);
70
+ const [emiError, setEmiError] = useState(null);
71
+ const { showToast } = useToast();
64
72
  React.useEffect(() => {
65
73
  setLoading(true);
66
74
  setFetchError(null);
@@ -76,6 +84,31 @@ function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
76
84
  .catch(() => setFetchError("Failed to load order details"))
77
85
  .finally(() => setLoading(false));
78
86
  }, [orderId, apiBase]);
87
+ const handleMarkInstallmentPaid = async (installmentIndex) => {
88
+ setMarkingPaidIndex(installmentIndex);
89
+ setEmiError(null);
90
+ try {
91
+ const res = await fetch(`${apiBase}/${orderId}/emi-installment`, {
92
+ method: "PATCH",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ installmentIndex }),
95
+ });
96
+ const json = await res.json().catch(() => ({}));
97
+ if (!res.ok)
98
+ throw new Error(json?.error ?? "Failed to mark installment paid");
99
+ setOrder(json?.data ?? json);
100
+ showToast("Installment marked paid.", "success");
101
+ }
102
+ catch (err) {
103
+ void normalizeError(err);
104
+ const message = err instanceof Error ? err.message : "Failed to mark installment paid.";
105
+ setEmiError(message);
106
+ showToast(message, "error");
107
+ }
108
+ finally {
109
+ setMarkingPaidIndex(null);
110
+ }
111
+ };
79
112
  const handleSave = async () => {
80
113
  if (!order)
81
114
  return;
@@ -118,7 +151,11 @@ function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
118
151
  };
119
152
  const addr = order?.shippingAddress ?? {};
120
153
  const addrLine = [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
121
- return (_jsxs(SideDrawer, { isOpen: true, title: `Order ${order?.id ?? orderId}`, onClose: onClose, children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toRupees(item.price ?? 0) })] }, i))) })] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toRupees(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Payment" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod })] })), _jsxs(Stack, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", gap: "3", children: [_jsx(Heading, { level: 4, size: "sm", weight: "semibold", children: "Update order" }), _jsx(Select, { label: "New status", value: newStatus, options: UPDATE_STATUS_OPTIONS, onChange: (e) => setNewStatus(e.target.value) }), _jsx(Input, { label: "Tracking number", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. 12345678901234" }), _jsx(Input, { label: "Carrier", value: carrier, onChange: (e) => setCarrier(e.target.value), placeholder: "e.g. Delhivery, Bluedart" }), _jsx(Input, { label: "Tracking URL (optional)", value: trackingUrl, onChange: (e) => setTrackingUrl(e.target.value), placeholder: "https://...", type: "url" }), saveError && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError }))] })] }), _jsxs(Row, { border: "top", paddingY: "y-sm-tall", padding: "x-md", align: "center", justify: "end", gap: "3", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Close" }), _jsx(Button, { onClick: handleSave, isLoading: saving, disabled: saving, children: "Save" })] })] }))] }));
154
+ return (_jsxs(SideDrawer, { isOpen: true, title: `Order ${order?.id ?? orderId}`, onClose: onClose, children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsxs(Stack, { gap: "none", children: [_jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status?.toUpperCase()] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: toRelativeDate(order.createdAt) })] }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-primary)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.items ?? []).map((item, i) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-secondary)]", children: ["Qty: ", item.quantity ?? 1] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toRupees(item.price ?? 0) })] }, i))) })] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toRupees(order.totalAmount ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Payment" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-secondary)]", transform: "capitalize", children: order.paymentMethod })] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toRupees(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toRupees(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toRupees(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
155
+ ? `Paid ${toRelativeDate(inst.paidAt)}`
156
+ : inst.dueDate
157
+ ? `Due ${toRelativeDate(inst.dueDate)}`
158
+ : "" })] }), _jsxs(Row, { align: "center", gap: "sm", className: "shrink-0", children: [_jsx(Badge, { variant: EMI_INSTALLMENT_BADGE_VARIANT[inst.status] ?? "default", children: inst.status }), inst.status !== "paid" && (_jsx(Button, { action: ACTIONS.STORE["mark-installment-paid"], size: "sm", variant: "outline", isLoading: markingPaidIndex === inst.index, disabled: markingPaidIndex !== null, onClick: () => handleMarkInstallmentPaid(inst.index), children: "Mark paid" }))] })] }, inst.index))) }), emiError && (_jsx(Div, { textSize: "xs", className: "mt-2 border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: emiError }))] })), _jsxs(Stack, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", gap: "3", children: [_jsx(Heading, { level: 4, size: "sm", weight: "semibold", children: "Update order" }), _jsx(Select, { label: "New status", value: newStatus, options: UPDATE_STATUS_OPTIONS, onChange: (e) => setNewStatus(e.target.value) }), _jsx(Input, { label: "Tracking number", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. 12345678901234" }), _jsx(Input, { label: "Carrier", value: carrier, onChange: (e) => setCarrier(e.target.value), placeholder: "e.g. Delhivery, Bluedart" }), _jsx(Input, { label: "Tracking URL (optional)", value: trackingUrl, onChange: (e) => setTrackingUrl(e.target.value), placeholder: "https://...", type: "url" }), saveError && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError }))] })] }), _jsxs(Row, { border: "top", paddingY: "y-sm-tall", padding: "x-md", align: "center", justify: "end", gap: "3", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Close" }), _jsx(Button, { onClick: handleSave, isLoading: saving, disabled: saving, children: "Save" })] })] }))] }));
122
159
  }
123
160
  // ---------------------------------------------------------------------------
124
161
  // Main view