@pisell/pisellos 0.0.568 → 0.0.569

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 (413) hide show
  1. package/dist/model/strategy/adapter/promotion/evaluator.js +3 -2
  2. package/dist/model/strategy/adapter/walletPass/utils.js +7 -4
  3. package/dist/modules/BaseModule.d.ts +2 -2
  4. package/dist/modules/BaseModule.js +4 -4
  5. package/dist/modules/BookingContext/index.d.ts +48 -0
  6. package/dist/modules/BookingContext/index.js +566 -0
  7. package/dist/modules/BookingContext/types.d.ts +102 -0
  8. package/dist/modules/BookingContext/types.js +51 -0
  9. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.d.ts +16 -0
  10. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +302 -0
  11. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +8 -0
  12. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +149 -0
  13. package/dist/modules/BookingContext/utils/cacheItemToBookingInput.d.ts +76 -0
  14. package/dist/modules/BookingContext/utils/cacheItemToBookingInput.js +249 -0
  15. package/dist/modules/BookingContext/utils/flexible.d.ts +28 -0
  16. package/dist/modules/BookingContext/utils/flexible.js +56 -0
  17. package/dist/modules/BookingContext/utils/formatResourceList.d.ts +8 -0
  18. package/dist/modules/BookingContext/utils/formatResourceList.js +38 -0
  19. package/dist/modules/BookingContext/utils/index.d.ts +11 -0
  20. package/dist/modules/BookingContext/utils/index.js +11 -0
  21. package/dist/modules/BookingContext/utils/noResource.d.ts +13 -0
  22. package/dist/modules/BookingContext/utils/noResource.js +77 -0
  23. package/dist/modules/BookingContext/utils/orderLineDisplay.d.ts +1 -0
  24. package/dist/modules/BookingContext/utils/orderLineDisplay.js +36 -0
  25. package/dist/modules/BookingContext/utils/productExtend.d.ts +72 -0
  26. package/dist/modules/BookingContext/utils/productExtend.js +339 -0
  27. package/dist/modules/BookingContext/utils/resourceErrors.d.ts +41 -0
  28. package/dist/modules/BookingContext/utils/resourceErrors.js +74 -0
  29. package/dist/modules/BookingContext/utils/resourceSelection.d.ts +7 -0
  30. package/dist/modules/BookingContext/utils/resourceSelection.js +24 -0
  31. package/dist/modules/BookingContext/utils/resources.d.ts +80 -0
  32. package/dist/modules/BookingContext/utils/resources.js +486 -0
  33. package/dist/modules/BookingContext/utils/serviceTimes.d.ts +55 -0
  34. package/dist/modules/BookingContext/utils/serviceTimes.js +151 -0
  35. package/dist/modules/BookingContext/utils/timeSlices.d.ts +42 -0
  36. package/dist/modules/BookingContext/utils/timeSlices.js +100 -0
  37. package/dist/modules/Cart/utils/cartProduct.js +26 -5
  38. package/dist/modules/Cart/utils/changePrice.js +27 -20
  39. package/dist/modules/Customer/index.d.ts +20 -0
  40. package/dist/modules/Customer/index.js +172 -83
  41. package/dist/modules/Customer/types.d.ts +2 -0
  42. package/dist/modules/Discount/index.d.ts +2 -1
  43. package/dist/modules/Discount/index.js +37 -9
  44. package/dist/modules/Discount/types.d.ts +1 -0
  45. package/dist/modules/OpenData/index.d.ts +8 -0
  46. package/dist/modules/OpenData/index.js +36 -16
  47. package/dist/modules/Order/index.d.ts +448 -23
  48. package/dist/modules/Order/index.js +5417 -561
  49. package/dist/modules/Order/types.d.ts +693 -88
  50. package/dist/modules/Order/types.js +68 -1
  51. package/dist/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
  52. package/dist/modules/Order/utils/bundleDiscountHydration.js +144 -0
  53. package/dist/modules/Order/utils/manualProductDiscount.d.ts +106 -0
  54. package/dist/modules/Order/utils/manualProductDiscount.js +210 -0
  55. package/dist/modules/Order/utils/orderCollectionIdentity.d.ts +53 -0
  56. package/dist/modules/Order/utils/orderCollectionIdentity.js +405 -0
  57. package/dist/modules/Order/utils.d.ts +167 -18
  58. package/dist/modules/Order/utils.js +960 -165
  59. package/dist/modules/Payment/index.d.ts +10 -9
  60. package/dist/modules/Payment/index.js +465 -703
  61. package/dist/modules/Payment/types.d.ts +40 -16
  62. package/dist/modules/Payment/utils.d.ts +1 -1
  63. package/dist/modules/Payment/utils.js +4 -3
  64. package/dist/modules/Payment/walletpass.js +44 -29
  65. package/dist/modules/Product/types.d.ts +29 -7
  66. package/dist/modules/ProductList/index.d.ts +17 -13
  67. package/dist/modules/ProductList/index.js +184 -74
  68. package/dist/modules/ProductList/types.d.ts +17 -0
  69. package/dist/modules/Quotation/index.d.ts +22 -2
  70. package/dist/modules/Quotation/index.js +108 -42
  71. package/dist/modules/Quotation/types.d.ts +19 -0
  72. package/dist/modules/ResourcePlanner/index.d.ts +24 -0
  73. package/dist/modules/ResourcePlanner/index.js +181 -0
  74. package/dist/modules/ResourcePlanner/planner.d.ts +14 -0
  75. package/dist/modules/ResourcePlanner/planner.js +1069 -0
  76. package/dist/modules/ResourcePlanner/types.d.ts +227 -0
  77. package/dist/modules/ResourcePlanner/types.js +1 -0
  78. package/dist/modules/Rules/index.d.ts +8 -10
  79. package/dist/modules/Rules/index.js +221 -109
  80. package/dist/modules/Rules/types.d.ts +14 -3
  81. package/dist/modules/SalesSummary/index.d.ts +23 -26
  82. package/dist/modules/SalesSummary/index.js +152 -46
  83. package/dist/modules/SalesSummary/types.d.ts +9 -1
  84. package/dist/modules/SalesSummary/utils.d.ts +3 -22
  85. package/dist/modules/SalesSummary/utils.js +803 -163
  86. package/dist/modules/Schedule/getDateIsInSchedule.js +14 -7
  87. package/dist/modules/Schedule/index.d.ts +18 -5
  88. package/dist/modules/Schedule/index.js +78 -24
  89. package/dist/modules/Schedule/types.d.ts +2 -0
  90. package/dist/modules/Summary/index.js +4 -3
  91. package/dist/modules/SurchargeList/index.d.ts +18 -0
  92. package/dist/modules/SurchargeList/index.js +185 -0
  93. package/dist/modules/SurchargeList/types.d.ts +15 -0
  94. package/dist/modules/SurchargeList/types.js +1 -0
  95. package/dist/modules/index.d.ts +3 -0
  96. package/dist/modules/index.js +4 -1
  97. package/dist/plugins/app.d.ts +2 -0
  98. package/dist/plugins/request.d.ts +1 -0
  99. package/dist/server/index.d.ts +462 -8
  100. package/dist/server/index.js +6112 -656
  101. package/dist/server/modules/floor-plan/index.d.ts +39 -0
  102. package/dist/server/modules/floor-plan/index.js +595 -0
  103. package/dist/server/modules/floor-plan/types.d.ts +43 -0
  104. package/dist/server/modules/floor-plan/types.js +13 -0
  105. package/dist/server/modules/index.d.ts +7 -2
  106. package/dist/server/modules/index.js +6 -0
  107. package/dist/server/modules/order/index.d.ts +207 -14
  108. package/dist/server/modules/order/index.js +2326 -379
  109. package/dist/server/modules/order/types.d.ts +76 -5
  110. package/dist/server/modules/order/types.js +14 -1
  111. package/dist/server/modules/order/utils/filterBookings.d.ts +7 -1
  112. package/dist/server/modules/order/utils/filterBookings.js +118 -11
  113. package/dist/server/modules/order/utils/filterOrders.js +242 -14
  114. package/dist/server/modules/payment/index.d.ts +28 -0
  115. package/dist/server/modules/payment/index.js +305 -0
  116. package/dist/server/modules/payment/types.d.ts +9 -0
  117. package/dist/server/modules/payment/types.js +1 -0
  118. package/dist/server/modules/products/index.d.ts +105 -14
  119. package/dist/server/modules/products/index.js +1393 -516
  120. package/dist/server/modules/products/types.d.ts +34 -1
  121. package/dist/server/modules/products/types.js +4 -0
  122. package/dist/server/modules/quotation/index.d.ts +39 -0
  123. package/dist/server/modules/quotation/index.js +278 -66
  124. package/dist/server/modules/resource/index.d.ts +2 -3
  125. package/dist/server/modules/resource/index.js +108 -86
  126. package/dist/server/modules/schedule/index.d.ts +6 -4
  127. package/dist/server/modules/schedule/index.js +116 -85
  128. package/dist/server/test.json +713 -0
  129. package/dist/server/types.d.ts +2 -0
  130. package/dist/server/utils/product.js +2 -2
  131. package/dist/server/utils/small-ticket.d.ts +71 -0
  132. package/dist/server/utils/small-ticket.js +963 -0
  133. package/dist/solution/BaseSales/index.d.ts +378 -0
  134. package/dist/solution/BaseSales/index.js +4006 -0
  135. package/dist/solution/BaseSales/types.d.ts +394 -0
  136. package/dist/solution/BaseSales/types.js +40 -0
  137. package/dist/solution/BaseSales/utils/cartPromotion.d.ts +287 -0
  138. package/dist/solution/BaseSales/utils/cartPromotion.js +1450 -0
  139. package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +144 -0
  140. package/dist/solution/BaseSales/utils/parseSalesResponse.js +157 -0
  141. package/dist/solution/BaseSales/utils/quotationPrice.d.ts +18 -0
  142. package/dist/solution/BaseSales/utils/quotationPrice.js +250 -0
  143. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +63 -0
  144. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +218 -0
  145. package/dist/solution/BaseSales/utils.d.ts +40 -0
  146. package/dist/solution/BaseSales/utils.js +263 -0
  147. package/dist/solution/BookingByStep/index.d.ts +1 -1
  148. package/dist/solution/BookingTicket/index.d.ts +394 -25
  149. package/dist/solution/BookingTicket/index.js +2506 -443
  150. package/dist/solution/BookingTicket/types.d.ts +191 -6
  151. package/dist/solution/BookingTicket/types.js +83 -10
  152. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +117 -0
  153. package/dist/solution/BookingTicket/utils/addProductDecision.js +346 -0
  154. package/dist/solution/BookingTicket/utils/addTimeAvailability.d.ts +35 -0
  155. package/dist/solution/BookingTicket/utils/addTimeAvailability.js +95 -0
  156. package/dist/solution/BookingTicket/utils/bookingStatus.d.ts +77 -0
  157. package/dist/solution/BookingTicket/utils/bookingStatus.js +136 -0
  158. package/dist/solution/BookingTicket/utils/cartView.d.ts +29 -0
  159. package/dist/solution/BookingTicket/utils/cartView.js +209 -0
  160. package/dist/solution/BookingTicket/utils/exampleData.d.ts +123 -0
  161. package/dist/solution/BookingTicket/utils/exampleData.js +183 -0
  162. package/dist/solution/BookingTicket/utils/orderCustomer.d.ts +4 -0
  163. package/dist/solution/BookingTicket/utils/orderCustomer.js +31 -0
  164. package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +185 -0
  165. package/dist/solution/BookingTicket/utils/resolveBestAddTimePlan.js +438 -0
  166. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +36 -0
  167. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +400 -0
  168. package/dist/solution/BookingTicket/utils/scan/cloudSearch.d.ts +4 -0
  169. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +41 -1
  170. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +32 -0
  171. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +56 -0
  172. package/dist/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
  173. package/dist/solution/BookingTicket/utils/scan/handleScan.js +11 -3
  174. package/dist/solution/BookingTicket/utils/scan/index.d.ts +5 -0
  175. package/dist/solution/BookingTicket/utils/scan/index.js +20 -8
  176. package/dist/solution/BookingTicket/utils/scan/scanProductValues.d.ts +21 -0
  177. package/dist/solution/BookingTicket/utils/scan/scanProductValues.js +150 -0
  178. package/dist/solution/BookingTicket/utils/sessionCatalogStale.d.ts +23 -0
  179. package/dist/solution/BookingTicket/utils/sessionCatalogStale.js +78 -0
  180. package/dist/solution/Checkout/index.d.ts +1 -0
  181. package/dist/solution/Checkout/index.js +25 -34
  182. package/dist/solution/Checkout/utils/index.js +4 -4
  183. package/dist/solution/RegisterAndLogin/config.js +2 -10
  184. package/dist/solution/Sales/index.d.ts +50 -5
  185. package/dist/solution/Sales/index.js +484 -92
  186. package/dist/solution/Sales/types.d.ts +26 -2
  187. package/dist/solution/ScanOrder/index.d.ts +27 -21
  188. package/dist/solution/ScanOrder/index.js +499 -434
  189. package/dist/solution/ScanOrder/types.d.ts +33 -11
  190. package/dist/solution/ScanOrder/types.js +4 -5
  191. package/dist/solution/ScanOrder/utils.d.ts +4 -1
  192. package/dist/solution/ScanOrder/utils.js +109 -34
  193. package/dist/solution/ShopDiscount/index.js +4 -4
  194. package/dist/solution/UnifiedBookingSales/index.d.ts +183 -0
  195. package/dist/solution/UnifiedBookingSales/index.js +1871 -0
  196. package/dist/solution/UnifiedBookingSales/types.d.ts +143 -0
  197. package/dist/solution/UnifiedBookingSales/types.js +11 -0
  198. package/dist/solution/VenueBooking/index.d.ts +29 -20
  199. package/dist/solution/VenueBooking/index.js +301 -213
  200. package/dist/solution/VenueBooking/sales-section-4-annotated.json +0 -2
  201. package/dist/solution/VenueBooking/utils/slotMerge.d.ts +6 -0
  202. package/dist/solution/VenueBooking/utils/slotMerge.js +9 -3
  203. package/dist/solution/index.d.ts +2 -0
  204. package/dist/solution/index.js +3 -1
  205. package/dist/types/index.d.ts +11 -1
  206. package/lib/model/strategy/adapter/promotion/evaluator.js +3 -2
  207. package/lib/model/strategy/adapter/walletPass/utils.js +6 -4
  208. package/lib/modules/BaseModule.d.ts +2 -2
  209. package/lib/modules/BaseModule.js +4 -4
  210. package/lib/modules/BookingContext/index.d.ts +48 -0
  211. package/lib/modules/BookingContext/index.js +390 -0
  212. package/lib/modules/BookingContext/types.d.ts +102 -0
  213. package/lib/modules/BookingContext/types.js +48 -0
  214. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.d.ts +16 -0
  215. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +286 -0
  216. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.d.ts +8 -0
  217. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +148 -0
  218. package/lib/modules/BookingContext/utils/cacheItemToBookingInput.d.ts +76 -0
  219. package/lib/modules/BookingContext/utils/cacheItemToBookingInput.js +242 -0
  220. package/lib/modules/BookingContext/utils/flexible.d.ts +28 -0
  221. package/lib/modules/BookingContext/utils/flexible.js +63 -0
  222. package/lib/modules/BookingContext/utils/formatResourceList.d.ts +8 -0
  223. package/lib/modules/BookingContext/utils/formatResourceList.js +35 -0
  224. package/lib/modules/BookingContext/utils/index.d.ts +11 -0
  225. package/lib/modules/BookingContext/utils/index.js +126 -0
  226. package/lib/modules/BookingContext/utils/noResource.d.ts +13 -0
  227. package/lib/modules/BookingContext/utils/noResource.js +78 -0
  228. package/lib/modules/BookingContext/utils/orderLineDisplay.d.ts +1 -0
  229. package/lib/modules/BookingContext/utils/orderLineDisplay.js +29 -0
  230. package/lib/modules/BookingContext/utils/productExtend.d.ts +72 -0
  231. package/lib/modules/BookingContext/utils/productExtend.js +330 -0
  232. package/lib/modules/BookingContext/utils/resourceErrors.d.ts +41 -0
  233. package/lib/modules/BookingContext/utils/resourceErrors.js +80 -0
  234. package/lib/modules/BookingContext/utils/resourceSelection.d.ts +7 -0
  235. package/lib/modules/BookingContext/utils/resourceSelection.js +24 -0
  236. package/lib/modules/BookingContext/utils/resources.d.ts +80 -0
  237. package/lib/modules/BookingContext/utils/resources.js +419 -0
  238. package/lib/modules/BookingContext/utils/serviceTimes.d.ts +55 -0
  239. package/lib/modules/BookingContext/utils/serviceTimes.js +161 -0
  240. package/lib/modules/BookingContext/utils/timeSlices.d.ts +42 -0
  241. package/lib/modules/BookingContext/utils/timeSlices.js +114 -0
  242. package/lib/modules/Cart/utils/cartProduct.js +27 -5
  243. package/lib/modules/Cart/utils/changePrice.js +20 -14
  244. package/lib/modules/Customer/index.d.ts +20 -0
  245. package/lib/modules/Customer/index.js +63 -10
  246. package/lib/modules/Customer/types.d.ts +2 -0
  247. package/lib/modules/Discount/index.d.ts +2 -1
  248. package/lib/modules/Discount/index.js +18 -2
  249. package/lib/modules/Discount/types.d.ts +1 -0
  250. package/lib/modules/OpenData/index.d.ts +8 -0
  251. package/lib/modules/OpenData/index.js +21 -3
  252. package/lib/modules/Order/index.d.ts +448 -23
  253. package/lib/modules/Order/index.js +3641 -308
  254. package/lib/modules/Order/types.d.ts +693 -88
  255. package/lib/modules/Order/types.js +55 -1
  256. package/lib/modules/Order/utils/bundleDiscountHydration.d.ts +5 -0
  257. package/lib/modules/Order/utils/bundleDiscountHydration.js +106 -0
  258. package/lib/modules/Order/utils/manualProductDiscount.d.ts +106 -0
  259. package/lib/modules/Order/utils/manualProductDiscount.js +207 -0
  260. package/lib/modules/Order/utils/orderCollectionIdentity.d.ts +53 -0
  261. package/lib/modules/Order/utils/orderCollectionIdentity.js +353 -0
  262. package/lib/modules/Order/utils.d.ts +167 -18
  263. package/lib/modules/Order/utils.js +974 -138
  264. package/lib/modules/Payment/index.d.ts +10 -9
  265. package/lib/modules/Payment/index.js +30 -124
  266. package/lib/modules/Payment/types.d.ts +40 -16
  267. package/lib/modules/Payment/utils.d.ts +1 -1
  268. package/lib/modules/Payment/utils.js +4 -3
  269. package/lib/modules/Payment/walletpass.js +16 -6
  270. package/lib/modules/Product/types.d.ts +29 -7
  271. package/lib/modules/ProductList/index.d.ts +17 -13
  272. package/lib/modules/ProductList/index.js +92 -51
  273. package/lib/modules/ProductList/types.d.ts +17 -0
  274. package/lib/modules/Quotation/index.d.ts +22 -2
  275. package/lib/modules/Quotation/index.js +76 -20
  276. package/lib/modules/Quotation/types.d.ts +19 -0
  277. package/lib/modules/ResourcePlanner/index.d.ts +24 -0
  278. package/lib/modules/ResourcePlanner/index.js +147 -0
  279. package/lib/modules/ResourcePlanner/planner.d.ts +14 -0
  280. package/lib/modules/ResourcePlanner/planner.js +895 -0
  281. package/lib/modules/ResourcePlanner/types.d.ts +227 -0
  282. package/lib/modules/ResourcePlanner/types.js +5 -0
  283. package/lib/modules/Rules/index.d.ts +8 -10
  284. package/lib/modules/Rules/index.js +152 -60
  285. package/lib/modules/Rules/types.d.ts +14 -3
  286. package/lib/modules/SalesSummary/index.d.ts +23 -26
  287. package/lib/modules/SalesSummary/index.js +84 -16
  288. package/lib/modules/SalesSummary/types.d.ts +9 -1
  289. package/lib/modules/SalesSummary/utils.d.ts +3 -22
  290. package/lib/modules/SalesSummary/utils.js +616 -111
  291. package/lib/modules/Schedule/getDateIsInSchedule.js +14 -7
  292. package/lib/modules/Schedule/index.d.ts +18 -5
  293. package/lib/modules/Schedule/index.js +49 -14
  294. package/lib/modules/Schedule/types.d.ts +2 -0
  295. package/lib/modules/Summary/index.js +2 -2
  296. package/lib/modules/SurchargeList/index.d.ts +18 -0
  297. package/lib/modules/SurchargeList/index.js +95 -0
  298. package/lib/modules/SurchargeList/types.d.ts +15 -0
  299. package/lib/modules/SurchargeList/types.js +5 -0
  300. package/lib/modules/index.d.ts +3 -0
  301. package/lib/modules/index.js +33 -0
  302. package/lib/plugins/app.d.ts +2 -0
  303. package/lib/plugins/request.d.ts +1 -0
  304. package/lib/server/index.d.ts +462 -8
  305. package/lib/server/index.js +3901 -205
  306. package/lib/server/modules/floor-plan/index.d.ts +39 -0
  307. package/lib/server/modules/floor-plan/index.js +306 -0
  308. package/lib/server/modules/floor-plan/types.d.ts +43 -0
  309. package/lib/server/modules/floor-plan/types.js +19 -0
  310. package/lib/server/modules/index.d.ts +7 -2
  311. package/lib/server/modules/index.js +21 -0
  312. package/lib/server/modules/order/index.d.ts +207 -14
  313. package/lib/server/modules/order/index.js +1324 -141
  314. package/lib/server/modules/order/types.d.ts +76 -5
  315. package/lib/server/modules/order/types.js +11 -1
  316. package/lib/server/modules/order/utils/filterBookings.d.ts +7 -1
  317. package/lib/server/modules/order/utils/filterBookings.js +119 -11
  318. package/lib/server/modules/order/utils/filterOrders.js +238 -13
  319. package/lib/server/modules/payment/index.d.ts +28 -0
  320. package/lib/server/modules/payment/index.js +168 -0
  321. package/lib/server/modules/payment/types.d.ts +9 -0
  322. package/lib/server/modules/payment/types.js +5 -0
  323. package/lib/server/modules/products/index.d.ts +105 -14
  324. package/lib/server/modules/products/index.js +588 -97
  325. package/lib/server/modules/products/types.d.ts +34 -1
  326. package/lib/server/modules/products/types.js +3 -0
  327. package/lib/server/modules/quotation/index.d.ts +39 -0
  328. package/lib/server/modules/quotation/index.js +145 -1
  329. package/lib/server/modules/resource/index.d.ts +2 -3
  330. package/lib/server/modules/resource/index.js +71 -45
  331. package/lib/server/modules/schedule/index.d.ts +6 -4
  332. package/lib/server/modules/schedule/index.js +55 -44
  333. package/lib/server/test.json +713 -0
  334. package/lib/server/types.d.ts +2 -0
  335. package/lib/server/utils/product.js +2 -2
  336. package/lib/server/utils/small-ticket.d.ts +71 -0
  337. package/lib/server/utils/small-ticket.js +926 -0
  338. package/lib/solution/BaseSales/index.d.ts +378 -0
  339. package/lib/solution/BaseSales/index.js +2367 -0
  340. package/lib/solution/BaseSales/types.d.ts +394 -0
  341. package/lib/solution/BaseSales/types.js +47 -0
  342. package/lib/solution/BaseSales/utils/cartPromotion.d.ts +287 -0
  343. package/lib/solution/BaseSales/utils/cartPromotion.js +1155 -0
  344. package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +144 -0
  345. package/lib/solution/BaseSales/utils/parseSalesResponse.js +140 -0
  346. package/lib/solution/BaseSales/utils/quotationPrice.d.ts +18 -0
  347. package/lib/solution/BaseSales/utils/quotationPrice.js +231 -0
  348. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +63 -0
  349. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +215 -0
  350. package/lib/solution/BaseSales/utils.d.ts +40 -0
  351. package/lib/solution/BaseSales/utils.js +259 -0
  352. package/lib/solution/BookingByStep/index.d.ts +1 -1
  353. package/lib/solution/BookingTicket/index.d.ts +394 -25
  354. package/lib/solution/BookingTicket/index.js +1350 -76
  355. package/lib/solution/BookingTicket/types.d.ts +191 -6
  356. package/lib/solution/BookingTicket/types.js +71 -10
  357. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +117 -0
  358. package/lib/solution/BookingTicket/utils/addProductDecision.js +353 -0
  359. package/lib/solution/BookingTicket/utils/addTimeAvailability.d.ts +35 -0
  360. package/lib/solution/BookingTicket/utils/addTimeAvailability.js +94 -0
  361. package/lib/solution/BookingTicket/utils/bookingStatus.d.ts +77 -0
  362. package/lib/solution/BookingTicket/utils/bookingStatus.js +136 -0
  363. package/lib/solution/BookingTicket/utils/cartView.d.ts +29 -0
  364. package/lib/solution/BookingTicket/utils/cartView.js +200 -0
  365. package/lib/solution/BookingTicket/utils/exampleData.d.ts +123 -0
  366. package/lib/solution/BookingTicket/utils/exampleData.js +185 -0
  367. package/lib/solution/BookingTicket/utils/orderCustomer.d.ts +4 -0
  368. package/lib/solution/BookingTicket/utils/orderCustomer.js +35 -0
  369. package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.d.ts +185 -0
  370. package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +387 -0
  371. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +36 -0
  372. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +220 -0
  373. package/lib/solution/BookingTicket/utils/scan/cloudSearch.d.ts +4 -0
  374. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +24 -3
  375. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +32 -0
  376. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +64 -0
  377. package/lib/solution/BookingTicket/utils/scan/handleScan.d.ts +3 -1
  378. package/lib/solution/BookingTicket/utils/scan/handleScan.js +4 -2
  379. package/lib/solution/BookingTicket/utils/scan/index.d.ts +5 -0
  380. package/lib/solution/BookingTicket/utils/scan/index.js +14 -2
  381. package/lib/solution/BookingTicket/utils/scan/scanProductValues.d.ts +21 -0
  382. package/lib/solution/BookingTicket/utils/scan/scanProductValues.js +115 -0
  383. package/lib/solution/BookingTicket/utils/sessionCatalogStale.d.ts +23 -0
  384. package/lib/solution/BookingTicket/utils/sessionCatalogStale.js +83 -0
  385. package/lib/solution/Checkout/index.d.ts +1 -0
  386. package/lib/solution/Checkout/index.js +18 -19
  387. package/lib/solution/Checkout/utils/index.js +4 -4
  388. package/lib/solution/RegisterAndLogin/config.js +2 -10
  389. package/lib/solution/Sales/index.d.ts +50 -5
  390. package/lib/solution/Sales/index.js +296 -39
  391. package/lib/solution/Sales/types.d.ts +26 -2
  392. package/lib/solution/ScanOrder/index.d.ts +27 -21
  393. package/lib/solution/ScanOrder/index.js +144 -103
  394. package/lib/solution/ScanOrder/types.d.ts +33 -11
  395. package/lib/solution/ScanOrder/types.js +4 -5
  396. package/lib/solution/ScanOrder/utils.d.ts +4 -1
  397. package/lib/solution/ScanOrder/utils.js +98 -26
  398. package/lib/solution/ShopDiscount/index.js +3 -3
  399. package/lib/solution/UnifiedBookingSales/index.d.ts +183 -0
  400. package/lib/solution/UnifiedBookingSales/index.js +1050 -0
  401. package/lib/solution/UnifiedBookingSales/types.d.ts +143 -0
  402. package/lib/solution/UnifiedBookingSales/types.js +14 -0
  403. package/lib/solution/VenueBooking/index.d.ts +29 -20
  404. package/lib/solution/VenueBooking/index.js +169 -94
  405. package/lib/solution/VenueBooking/sales-section-4-annotated.json +0 -2
  406. package/lib/solution/VenueBooking/utils/slotMerge.d.ts +6 -0
  407. package/lib/solution/VenueBooking/utils/slotMerge.js +9 -3
  408. package/lib/solution/index.d.ts +2 -0
  409. package/lib/solution/index.js +22 -0
  410. package/lib/types/index.d.ts +11 -1
  411. package/package.json +1 -1
  412. package/dist/solution/Checkout/appointmentDemo.json +0 -1
  413. package/lib/solution/Checkout/appointmentDemo.json +0 -1
@@ -5,15 +5,53 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.OrderModule = void 0;
7
7
  var _BaseModule = require("../BaseModule");
8
+ var _types = require("./types");
8
9
  var _utils = require("./utils");
9
10
  var _utils2 = require("../Product/utils");
10
11
  var _dayjs = _interopRequireDefault(require("dayjs"));
12
+ var _cartPromotion = require("../../solution/BaseSales/utils/cartPromotion");
11
13
  var _utils3 = require("../../solution/ScanOrder/utils");
14
+ var _manualProductDiscount = require("./utils/manualProductDiscount");
15
+ var _bundleDiscountHydration = require("./utils/bundleDiscountHydration");
16
+ var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
12
17
  var _Discount = require("../Discount");
13
18
  var _Rules = require("../Rules");
14
- var _types = require("../Rules/types");
19
+ var _types2 = require("../Rules/types");
15
20
  var _decimal = _interopRequireDefault(require("decimal.js"));
21
+ var _lodashEs = require("lodash-es");
16
22
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
23
+ function normalizeDiscountListSignature(discountList) {
24
+ return (discountList || []).map(item => ({
25
+ type: item?.type ?? '',
26
+ discount_id: item?.discount_id ?? null,
27
+ order_discount_id: item?.order_discount_id ?? null,
28
+ amount: new _decimal.default(Number(item?.amount) || 0).toFixed(2)
29
+ }));
30
+ }
31
+ function isSameDiscountList(left, right) {
32
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
33
+ return JSON.stringify(normalizeDiscountListSignature(left)) === JSON.stringify(normalizeDiscountListSignature(right));
34
+ }
35
+ function isManualProductDiscountProduct(product) {
36
+ return product?.metadata?.is_manual_discount === true || product?.metadata?.is_manual_discount === 1 || (product?.discount_list || []).some(item => item?.type === 'product');
37
+ }
38
+ const ORDER_PAYMENT_STATUS_PAID = 'paid';
39
+ const ORDER_PAYMENT_STATUS_PENDING = 'payment_pending';
40
+ function isVoucherPaymentRecord(payment) {
41
+ return payment?.voucher_id !== undefined && Number(payment.voucher_id) !== 0;
42
+ }
43
+ function hasSyncedOrderPaymentRecord(payment) {
44
+ return payment?.order_payment_id !== undefined && payment.order_payment_id !== null;
45
+ }
46
+ function isPendingVoucherPaymentRecord(payment) {
47
+ return isVoucherPaymentRecord(payment) && payment?.status === ORDER_PAYMENT_STATUS_PENDING;
48
+ }
49
+ function isPaidPaymentRecord(payment) {
50
+ return payment?.status === ORDER_PAYMENT_STATUS_PAID;
51
+ }
52
+ function isCommittedVoucherPaymentRecord(payment) {
53
+ return isVoucherPaymentRecord(payment) && payment?.status !== 'voided' && (hasSyncedOrderPaymentRecord(payment) || isPaidPaymentRecord(payment));
54
+ }
17
55
  class OrderModule extends _BaseModule.BaseModule {
18
56
  defaultName = 'order';
19
57
  defaultVersion = '1.0.0';
@@ -24,6 +62,114 @@ class OrderModule extends _BaseModule.BaseModule {
24
62
  cacheId;
25
63
  salesSummaryModuleName;
26
64
  rulesHooksOverride;
65
+ moduleHooksOverride;
66
+ orderHooks;
67
+ otherParams;
68
+ // ─── 促销/赠品 ────────────────────────────────────
69
+ /** 评估器引用;由 SalesSdkProvider 注入,未注入时 applyPromotion 静默跳过 */
70
+ promotionEvaluator = null;
71
+ /** 赠品选择 resolver;由 SDK host bridge 注入 */
72
+ giftSelectResolver = null;
73
+ /** applyPromotion 递归保护 flag(写路径同步调 applyPromotion,禁止重入) */
74
+ isApplyingPromotion = false;
75
+ /** 商品曾应用过客户券卡,客户变更剥离后需强制跑一次 calcDiscount 复位价格 */
76
+ hasActiveCustomerBoundDiscount = false;
77
+ /** 最近一次 applyPromotion 的未满足促销提示 */
78
+ lastUnfulfilledPromotions = [];
79
+ /** 最近一次 applyPromotion 的赠品操作 diff(debug / SDK 读取使用) */
80
+ lastGiftActions = null;
81
+ /**
82
+ * 是否将 tempOrder 写入 IndexedDB 草稿(orders store)。
83
+ * BigSale 弹窗等短生命周期场景通过 setEnableTempOrderPersist(false) 关闭。
84
+ */
85
+ draftPersistEnabled = true;
86
+ collectHydratedEditDiscounts(products) {
87
+ const discountMap = new Map();
88
+ for (const product of (0, _bundleDiscountHydration.expandHydratedProductsForDiscountCollection)(products)) {
89
+ for (const item of product?.discount_list || []) {
90
+ const type = item?.type ?? item?.tag;
91
+ if (!type || type === 'product') continue;
92
+ const resourceId = item?.discount?.resource_id ?? item?.resource_id ?? item?.discount_id ?? item?.order_discount_id;
93
+ if (resourceId === undefined || resourceId === null || resourceId === '') continue;
94
+ const id = Number(resourceId);
95
+ const key = Number.isFinite(id) ? id : String(resourceId);
96
+ const title = item?.discount?.title ?? {};
97
+ const amount = Number(item?.amount || 0);
98
+ const detail = {
99
+ amount: String(item?.amount ?? '0'),
100
+ type,
101
+ resource_id: key,
102
+ title,
103
+ original_amount: String(item?.discount?.original_amount ?? item?.amount ?? '0'),
104
+ num: Number(item?.metadata?.num ?? product?.num ?? product?.product_quantity ?? 1) || 1,
105
+ metadata: item?.metadata,
106
+ discount: item?.discount,
107
+ _num: Number(item?.metadata?.num ?? product?.num ?? product?.product_quantity ?? 1) || 1
108
+ };
109
+ const existed = discountMap.get(key);
110
+ if (existed) {
111
+ existed.amount = Number(existed.amount || 0) + amount;
112
+ existed.used_par_value = String(Number(existed.used_par_value || 0) + amount);
113
+ existed.balance = String(Number(existed.balance || 0) + amount);
114
+ const productIds = Array.isArray(existed.limited_relation_product_data?.product_ids) ? existed.limited_relation_product_data.product_ids : [];
115
+ if (product?.product_id !== undefined && product?.product_id !== null && !productIds.includes(product.product_id)) {
116
+ existed.limited_relation_product_data = {
117
+ ...(existed.limited_relation_product_data || {}),
118
+ product_ids: [...productIds, product.product_id]
119
+ };
120
+ }
121
+ existed.applicableProductDetails = [...(existed.applicableProductDetails || []), detail];
122
+ existed.appliedProductDetails = [...(existed.appliedProductDetails || []), detail];
123
+ continue;
124
+ }
125
+ discountMap.set(key, {
126
+ id: key,
127
+ product_name: '',
128
+ encoded: '',
129
+ code: '',
130
+ tag: type,
131
+ product_id: item?.discount?.discount_product_id ?? product?.product_id ?? 0,
132
+ relation_type: '',
133
+ par_value: String(item?.discount?.original_amount ?? item?.amount ?? '0'),
134
+ used_par_value: String(item?.amount ?? '0'),
135
+ limit_status: 'enable',
136
+ limited_relation_product_data: {
137
+ id: 0,
138
+ type: 'products',
139
+ product_ids: [product?.product_id].filter(value => value !== undefined && value !== null),
140
+ product_collection_id: [],
141
+ filter: 0,
142
+ exclude_product_ids: []
143
+ },
144
+ balance: String(item?.amount ?? '0'),
145
+ format_title: title,
146
+ product: {
147
+ id: product?.product_id ?? 0,
148
+ title: '',
149
+ is_charge_tax: product?.is_charge_tax ?? 0,
150
+ base_price: String(product?.original_price ?? product?.selling_price ?? '0')
151
+ },
152
+ type,
153
+ resource_id: key,
154
+ isEditMode: true,
155
+ isSelected: true,
156
+ isAvailable: true,
157
+ isUsed: true,
158
+ isDisabled: true,
159
+ amount,
160
+ discount: {
161
+ ...(item?.discount || {}),
162
+ resource_id: key,
163
+ discount_product_id: item?.discount?.discount_product_id ?? product?.product_id ?? 0,
164
+ discount_calculation_mode: item?.discount?.discount_calculation_mode ?? 'item_level'
165
+ },
166
+ applicableProductDetails: [detail],
167
+ appliedProductDetails: [detail]
168
+ });
169
+ }
170
+ }
171
+ return Array.from(discountMap.values());
172
+ }
27
173
 
28
174
  /**
29
175
  * Populate `savedAmount` on each discount based on product-level discount details.
@@ -31,22 +177,79 @@ class OrderModule extends _BaseModule.BaseModule {
31
177
  */
32
178
  static populateSavedAmounts(productList, discountList) {
33
179
  const savedMap = new Map();
180
+ const editModeSavedMap = new Map();
181
+ const runtimeSavedMap = new Map();
182
+ const editModeDiscountKeys = new Set((discountList || []).filter(discount => discount?.isEditMode).map(discount => discount?.id).filter(id => id !== undefined && id !== null));
183
+ const addDiscountAmount = (targetMap, discountKey, amount) => {
184
+ targetMap.set(discountKey, (targetMap.get(discountKey) || new _decimal.default(0)).plus(amount));
185
+ };
186
+ const addProductDiscountAmount = (discount, quantity, isPersistedProduct) => {
187
+ const discountKey = discount.discount?.resource_id || discount.id;
188
+ if (discountKey == null) return;
189
+ const qty = Number(quantity) || 1;
190
+ const amount = new _decimal.default(discount.amount || 0).times(qty).plus(discount.metadata?.product_discount_difference || 0);
191
+ addDiscountAmount(savedMap, discountKey, amount);
192
+ addDiscountAmount(isPersistedProduct || discount.order_discount_id != null ? editModeSavedMap : runtimeSavedMap, discountKey, amount);
193
+ };
34
194
  for (const product of productList) {
35
195
  const qty = product.num || 1;
196
+ const isPersistedProduct = Boolean(product.order_detail_id || product.orderDetailId || product.booking_id);
36
197
  for (const pd of product.discount_list || []) {
37
- const discountKey = pd.discount?.resource_id || pd.id;
38
- if (discountKey == null) continue;
39
- const amount = new _decimal.default(pd.amount || 0).times(qty).plus(pd.metadata?.product_discount_difference || 0);
40
- savedMap.set(discountKey, (savedMap.get(discountKey) || new _decimal.default(0)).plus(amount));
198
+ addProductDiscountAmount(pd, qty, isPersistedProduct);
199
+ }
200
+ for (const bundle of product.product_bundle || []) {
201
+ const isPersistedBundleProduct = isPersistedProduct || Boolean(bundle?.order_detail_id || bundle?.orderDetailId || bundle?.booking_id);
202
+ const bundleQty = new _decimal.default(Number(qty) || 1).times(Number(bundle?.num ?? bundle?.quantity ?? 1) || 1).toNumber();
203
+ for (const pd of bundle?.discount_list || []) {
204
+ addProductDiscountAmount(pd, bundleQty, isPersistedBundleProduct);
205
+ }
41
206
  }
42
207
  }
43
208
  for (const d of discountList) {
44
209
  const key = d.id;
45
- d.savedAmount = d.isSelected && key != null && savedMap.has(key) ? savedMap.get(key).toNumber() : 0;
210
+ const sourceMap = d.isEditMode ? editModeSavedMap : editModeDiscountKeys.has(key) ? runtimeSavedMap : savedMap;
211
+ d.savedAmount = d.isSelected && key != null && sourceMap.has(key) ? sourceMap.get(key).toNumber() : 0;
46
212
  }
47
213
  }
214
+ applyProductDiscountPrices(products) {
215
+ return (products || []).map(product => {
216
+ if (isManualProductDiscountProduct(product)) return product;
217
+ const discountItems = Array.isArray(product.discount_list) ? product.discount_list : [];
218
+ const hasOnlyPersistedDiscountSnapshots = product.order_detail_id !== undefined && product.order_detail_id !== null && discountItems.length > 0 && discountItems.every(discount => discount?.order_discount_id !== undefined && discount?.order_discount_id !== null);
219
+ if (hasOnlyPersistedDiscountSnapshots) {
220
+ return product;
221
+ }
222
+ const totalPerUnitDiscount = (0, _utils.resolveEffectivePerUnitDiscount)(product);
223
+ const optionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(product));
224
+ const sourcePrice = product.metadata?.source_product_price ?? (product.metadata?.main_product_original_price != null ? new _decimal.default(Number(product.metadata.main_product_original_price) || 0).minus(optionSum).toFixed(2) : product.original_price ?? '0');
225
+ const newSourceSellingPrice = new _decimal.default(Number(sourcePrice) || 0).minus(totalPerUnitDiscount).toDecimalPlaces(2).toString();
226
+ const newMainSellingPrice = new _decimal.default(Number(newSourceSellingPrice) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
227
+ if (product.metadata) {
228
+ product.metadata.main_product_selling_price = newMainSellingPrice;
229
+ product.metadata.price_schema_version = 2;
230
+ }
231
+ if (Array.isArray(product.product_bundle)) {
232
+ product.product_bundle = product.product_bundle.map(bundle => {
233
+ const hasBundleDiscount = Array.isArray(bundle?.discount_list) && bundle.discount_list.length > 0;
234
+ if (hasBundleDiscount) return bundle;
235
+ const restoredPrice = bundle?.original_price ?? bundle?.product_price ?? bundle?.price;
236
+ if (restoredPrice === undefined || restoredPrice === null || restoredPrice === '') return bundle;
237
+ return {
238
+ ...bundle,
239
+ price: restoredPrice,
240
+ bundle_selling_price: restoredPrice
241
+ };
242
+ });
243
+ }
244
+ product.selling_price = (0, _utils.composeLinePrice)({
245
+ mainPrice: newMainSellingPrice,
246
+ bundle: product.product_bundle
247
+ });
248
+ return product;
249
+ });
250
+ }
48
251
  constructor(name, version) {
49
- super(name || "order", version);
252
+ super(name || 'order', version);
50
253
  }
51
254
  async initialize(core, options) {
52
255
  this.core = core;
@@ -54,25 +257,66 @@ class OrderModule extends _BaseModule.BaseModule {
54
257
  if (!this.store.tempOrder) {
55
258
  this.store.tempOrder = null;
56
259
  }
260
+ if (!this.store.summary) {
261
+ this.store.summary = (0, _utils.createEmptySummary)();
262
+ }
57
263
  if (!Array.isArray(this.store.availableWalletIds)) {
58
264
  this.store.availableWalletIds = [];
59
265
  }
60
266
  this.request = this.core.getPlugin('request');
267
+ const appPlugin = this.core.getPlugin('app');
268
+ if (!appPlugin) {
269
+ console.error('Order 模块需要 app 插件支持');
270
+ }
271
+ const app = appPlugin?.getApp?.();
272
+ this.logger = app?.logger;
61
273
  this.window = this.core.getPlugin('window');
62
274
  const otherParams = options.otherParams || {};
63
275
  this.cacheId = otherParams.cacheId;
64
276
  this.salesSummaryModuleName = otherParams.salesSummaryModuleName;
65
277
  this.rulesHooksOverride = otherParams.rules?.hooks;
278
+ this.moduleHooksOverride = options.hooks;
279
+ this.orderHooks = this.moduleHooksOverride || otherParams.order?.hooks || otherParams.hooks?.order;
280
+ this.otherParams = otherParams;
66
281
  this.registerDiscountModules(options);
67
- this.restoreTempOrderFromStorage();
68
- const appPlugin = this.core.getPlugin('app');
69
- if (!appPlugin) {
70
- console.warn('Order 模块需要 app 插件支持');
71
- }
72
- const app = appPlugin?.getApp();
73
- if (app) this.logger = app.logger;
74
282
  this.logInfo('OrderModule initialized successfully');
75
283
  }
284
+ async destroy() {
285
+ const childModules = [this.store?.discount, this.store?.rules];
286
+ for (const childModule of childModules) {
287
+ if (!childModule || typeof childModule.destroy !== 'function') continue;
288
+ try {
289
+ // Order 内部注册的子模块需要跟随父模块释放,避免 core 中残留独立模块。
290
+ await childModule.destroy();
291
+ } catch (error) {
292
+ const errorMessage = error instanceof Error ? error.message : String(error);
293
+ this.logWarning('Destroy child module failed', {
294
+ module: childModule.name,
295
+ error: errorMessage
296
+ });
297
+ }
298
+ }
299
+ if (this.store) {
300
+ this.store.discount = null;
301
+ this.store.rules = null;
302
+ }
303
+ super.destroy();
304
+ }
305
+
306
+ /**
307
+ * 更新父级解决方案透传的运行态参数,并同步 Order 内部依赖的派生字段。
308
+ *
309
+ * @example
310
+ * orderModule.updateOtherParams({ cacheId: 'cache-2', channel: 'pos' });
311
+ */
312
+ updateOtherParams(params) {
313
+ const otherParams = params || {};
314
+ this.cacheId = otherParams.cacheId;
315
+ this.salesSummaryModuleName = otherParams.salesSummaryModuleName;
316
+ this.rulesHooksOverride = otherParams.rules?.hooks;
317
+ this.orderHooks = this.moduleHooksOverride || otherParams.order?.hooks || otherParams.hooks?.order;
318
+ this.otherParams = otherParams;
319
+ }
76
320
 
77
321
  /**
78
322
  * 记录信息日志
@@ -151,30 +395,70 @@ class OrderModule extends _BaseModule.BaseModule {
151
395
  // ─── Discount: 公共 API ───
152
396
 
153
397
  async loadDiscountConfig(params) {
154
- const prepareConfig = await this.store.discount?.loadPrepareConfig({
398
+ const orderId = params.orderId || this.store.tempOrder?.order_id || undefined;
399
+ const prepareConfigResult = await this.store.discount?.loadPrepareConfig({
155
400
  customer_id: params.customerId,
156
- action: params.action || 'create',
401
+ action: params.action || (orderId ? 'edit' : 'create'),
157
402
  with_good_pass: 1,
158
403
  with_discount_card: 1,
159
404
  with_wallet_pass_holder: 1,
160
405
  with_available_wallet_flag: 1,
161
406
  with_available_wallet_pass_flag: 1,
162
- request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
407
+ request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
408
+ ...(orderId ? {
409
+ order_id: orderId
410
+ } : {})
163
411
  });
164
- const discountList = prepareConfig?.discountList;
165
- this.store.availableWalletIds = prepareConfig?.availableWalletIds || [];
412
+ const discountList = prepareConfigResult?.discountList;
413
+ this.store.availableWalletIds = prepareConfigResult?.availableWalletIds || [];
166
414
  if (discountList) {
167
- this.store.discount?.setDiscountList(discountList);
415
+ const currentDiscountList = this.store.discount?.getDiscountList() || [];
416
+ const currentEditDiscounts = currentDiscountList.filter(discount => discount?.isEditMode);
417
+ const currentScanDiscounts = currentDiscountList.filter(discount => discount?.isScan);
418
+ const currentScanDiscountIds = new Set(currentScanDiscounts.map(discount => discount?.id));
419
+ const runtimeDiscountIds = new Set();
420
+ const runtimeDiscounts = discountList.filter(discount => {
421
+ if (currentScanDiscountIds.has(discount?.id)) return false;
422
+ if (runtimeDiscountIds.has(discount?.id)) return false;
423
+ runtimeDiscountIds.add(discount?.id);
424
+ return true;
425
+ });
426
+ const mergedDiscountList = [...currentEditDiscounts, ...currentScanDiscounts, ...runtimeDiscounts];
427
+ await this.store.discount?.setOriginalDiscountList(mergedDiscountList);
428
+ await this.store.discount?.setDiscountList(mergedDiscountList);
168
429
  }
169
- if (this.store.tempOrder?.products?.length) {
430
+ if (params.apply !== false && this.store.tempOrder?.products?.length) {
170
431
  this.applyDiscount();
171
432
  }
433
+ return this.getDiscountList();
172
434
  }
173
435
  getDiscountList() {
174
436
  return this.store.discount?.getDiscountList() || [];
175
437
  }
176
438
  getAvailableWalletIds() {
177
- return this.store.availableWalletIds || [];
439
+ return Array.isArray(this.store.availableWalletIds) ? [...this.store.availableWalletIds] : [];
440
+ }
441
+ async syncScannedDiscountsToOriginalDiscountList(discountList) {
442
+ const scanDiscounts = (discountList || []).filter(discount => discount?.isScan);
443
+ if (!scanDiscounts.length) return;
444
+ const scanDiscountIds = new Set(scanDiscounts.map(discount => discount?.id));
445
+ const originalDiscounts = this.store.discount?.getOriginalDiscountList?.() || [];
446
+ const nextOriginalDiscounts = [...scanDiscounts, ...originalDiscounts.filter(discount => !scanDiscountIds.has(discount?.id))];
447
+ await this.store.discount?.setOriginalDiscountList?.(nextOriginalDiscounts);
448
+ }
449
+ async ensureEditDiscountConfigForProductChange(tempOrder) {
450
+ const orderId = tempOrder.order_id;
451
+ const customerId = Number(tempOrder.customer_id || tempOrder.customer?.id || tempOrder._extend?.customerSnapshot?.id || tempOrder._extend?.customerSnapshot?.customer_id || 0);
452
+ if (!orderId || !customerId || customerId === 1) return;
453
+ const currentDiscountList = this.store.discount?.getDiscountList() || [];
454
+ const hasRuntimeDiscounts = currentDiscountList.some(discount => !discount?.isEditMode);
455
+ if (hasRuntimeDiscounts) return;
456
+ await this.loadDiscountConfig({
457
+ customerId,
458
+ action: 'edit',
459
+ orderId: Number(orderId),
460
+ apply: false
461
+ });
178
462
  }
179
463
  async scanCode(code, customerId) {
180
464
  const resultDiscountWithReason = (await this.store.discount?.batchSearch(code, {
@@ -193,7 +477,8 @@ class OrderModule extends _BaseModule.BaseModule {
193
477
  type: 'clientCalc',
194
478
  isAvailable: false,
195
479
  discountList: this.getDiscountList(),
196
- unavailableReason: _types.UnavailableReason.Unknown
480
+ scannedDiscountList: resultDiscountList,
481
+ unavailableReason: _types2.UnavailableReason.Unknown
197
482
  };
198
483
  }
199
484
  if (!resultDiscountList.length) {
@@ -201,6 +486,7 @@ class OrderModule extends _BaseModule.BaseModule {
201
486
  type: 'server',
202
487
  isAvailable: false,
203
488
  discountList: this.getDiscountList(),
489
+ scannedDiscountList: resultDiscountList,
204
490
  unavailableReasonKey
205
491
  };
206
492
  }
@@ -210,16 +496,16 @@ class OrderModule extends _BaseModule.BaseModule {
210
496
  }));
211
497
  const currentSelected = this.getDiscountList().filter(n => n.isSelected);
212
498
  if (currentSelected.length && currentSelected.some(n => withScanList.some(m => m.id === n.id))) {
499
+ await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
213
500
  return {
214
501
  type: 'clientCalc',
215
502
  isAvailable: true,
216
- discountList: this.getDiscountList()
503
+ discountList: this.getDiscountList(),
504
+ scannedDiscountList: resultDiscountList
217
505
  };
218
506
  }
219
507
  const tempOrder = this.store.tempOrder;
220
- const holders = tempOrder?.holder?.form_record_id ? [{
221
- form_record_id: tempOrder.holder.form_record_id
222
- }] : [];
508
+ const holders = tempOrder?.holder?.form_record || [];
223
509
  const {
224
510
  isAvailable,
225
511
  discountList: newDiscountList,
@@ -228,9 +514,9 @@ class OrderModule extends _BaseModule.BaseModule {
228
514
  productList: tempOrder?.products || [],
229
515
  oldDiscountList: this.getDiscountList(),
230
516
  newDiscountList: withScanList,
517
+ orderTotalAmount: Number(this.store.summary?.total_amount || 0),
231
518
  holders,
232
- isFormSubject: !!tempOrder?.holder?.type && tempOrder.holder.type === 'form',
233
- orderTotalAmount: Number(tempOrder?.summary?.total_amount || 0)
519
+ isFormSubject: !!tempOrder?.holder?.type && tempOrder.holder.type === 'form'
234
520
  }) || {
235
521
  isAvailable: false,
236
522
  discountList: this.getDiscountList()
@@ -238,111 +524,255 @@ class OrderModule extends _BaseModule.BaseModule {
238
524
  if (isAvailable && newDiscountList) {
239
525
  this.store.discount?.setDiscountList(newDiscountList);
240
526
  this.applyDiscount();
527
+ await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
241
528
  }
242
529
  return {
243
530
  type: 'clientCalc',
244
531
  isAvailable: isAvailable || false,
245
532
  discountList: newDiscountList || this.getDiscountList(),
533
+ scannedDiscountList: resultDiscountList,
246
534
  unavailableReason
247
535
  };
248
536
  }
249
- applyDiscount() {
537
+ applyDiscount(options) {
250
538
  const tempOrder = this.store.tempOrder;
251
539
  // 任意 products CRUD 后都应当重算 discount 状态;即使 products 为空,
252
540
  // 也需要让 Rules.calcDiscount 把 DiscountModule 的 isSelected / isAvailable /
253
- // appliedProductDetails / savedAmount 以及 tempOrder.discount_list 复位。
541
+ // appliedProductDetails / savedAmount 复位。
254
542
  if (!tempOrder) return;
543
+ this.syncLinkedBookingHoldersToProducts(tempOrder);
544
+ delete tempOrder.discount_list;
545
+ const discountList = this.store.discount?.getDiscountList() || [];
546
+ const shouldSkipDiscountCalculation = this.shouldSkipDiscountCalculation(tempOrder);
547
+ if (shouldSkipDiscountCalculation) return;
255
548
  const rulesModule = this.store.rules;
256
549
  if (!rulesModule) return;
257
- const discountList = this.store.discount?.getDiscountList() || [];
258
- const holders = tempOrder.holder?.form_record_id ? [{
259
- form_record_id: tempOrder.holder.form_record_id
260
- }] : [];
261
550
  const result = rulesModule.calcDiscount({
262
551
  productList: tempOrder.products,
263
552
  discountList,
264
- holders,
265
- isFormSubject: !!tempOrder.holder?.type && tempOrder.holder.type === 'form',
266
- orderTotalAmount: Number(tempOrder?.summary?.total_amount || 0)
553
+ holders: [],
554
+ isFormSubject: product => (0, _utils.resolveIsFormSubject)(tempOrder, product),
555
+ orderTotalAmount: Number(this.store.summary?.total_amount || 0)
556
+ }, {
557
+ reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
267
558
  });
268
559
  if (result?.productList) {
269
- tempOrder.products = result.productList;
560
+ const previousProductsByUid = (0, _utils.indexOrderProductsByUid)(tempOrder.products);
561
+ tempOrder.products = this.applyProductDiscountPrices((result.productList || []).map((product, index) => {
562
+ const uid = (0, _utils.getOrderProductLineUid)(product);
563
+ const previous = (uid ? previousProductsByUid.get(uid) : undefined) ?? previousProductsByUid.get(`__index__:${index}`);
564
+ if (isManualProductDiscountProduct(previous)) {
565
+ return previous;
566
+ }
567
+ const mergedProduct = previous ? (0, _utils.mergeOrderProductDisplayFields)(previous, product) : product;
568
+ return {
569
+ ...mergedProduct,
570
+ discount_list: (0, _utils.normalizeOrderProductDiscountList)((0, _manualProductDiscount.syncManualProductDiscountProductNum)(mergedProduct.discount_list, mergedProduct.num))
571
+ };
572
+ }));
270
573
  }
271
574
  if (result?.discountList) {
272
575
  OrderModule.populateSavedAmounts(result.productList || tempOrder.products, result.discountList);
273
576
  this.store.discount?.setDiscountList(result.discountList);
274
- tempOrder.discount_list = result.discountList.filter(d => d.isSelected);
275
577
  }
578
+ this.hasActiveCustomerBoundDiscount = (tempOrder.products || []).some(product => this.productHasCustomerBoundDiscount(product));
276
579
  }
277
580
 
278
- // ─── TempOrder: 初始化入口 ───
581
+ // ─── 促销/赠品 ──────────────────────────────────────
279
582
 
280
- initTempOrder(params) {
281
- if (params.cacheId !== undefined) this.cacheId = params.cacheId;
282
- if (params.salesSummaryModuleName !== undefined) this.salesSummaryModuleName = params.salesSummaryModuleName;
283
- this.restoreTempOrderFromStorage();
583
+ /**
584
+ * 注入促销评估器。
585
+ *
586
+ * @example
587
+ * order.setPromotionEvaluator(appHelper.utils.promotionEvaluator);
588
+ */
589
+ setPromotionEvaluator(evaluator) {
590
+ this.promotionEvaluator = evaluator || null;
284
591
  }
285
592
 
286
- // ─── TempOrder: localStorage 持久化 ───
593
+ /**
594
+ * 注入赠品选择 resolver。OS 需要补赠品时会 await 调用 resolver;缺省时跳过新增并 console.warn。
595
+ *
596
+ * @example
597
+ * order.setGiftSelectResolver(async (ctx) => {
598
+ * // SDK 内部决定弹窗 or 自动选第一项,返回完整 OrderProduct[]
599
+ * return giftSelectBridge.request(ctx);
600
+ * });
601
+ */
602
+ setGiftSelectResolver(resolver) {
603
+ this.giftSelectResolver = resolver || null;
604
+ }
287
605
 
288
- getTempOrderStorageKey() {
289
- if (!this.cacheId) return null;
290
- return `scanOrder:tempOrder:${this.cacheId}`;
606
+ /**
607
+ * 为商品目录追加促销标签,供上层 Sales 门面复用。
608
+ *
609
+ * @example
610
+ * const products = order.appendPromotionTags(catalogProducts);
611
+ */
612
+ appendPromotionTags(products) {
613
+ return (0, _cartPromotion.appendPromotionTags)(products, this.promotionEvaluator);
291
614
  }
292
- restoreTempOrderFromStorage() {
293
- const key = this.getTempOrderStorageKey();
294
- if (!key) return;
295
- if (!this.window) return;
296
- const cachedData = this.window.localStorage.getItem(key);
297
- if (!cachedData) return;
615
+
616
+ /**
617
+ * 应用购物车促销:计算 → 差异化赠品 → 写回 tempOrder.products → emit onPromotionApplied。
618
+ *
619
+ * 调用时机:写路径(add/update/remove/clear/setCustomer)末尾自动触发;外部一般无需手动调。
620
+ *
621
+ * 关键约束:
622
+ * - 不调用任何公开 CRUD API(如 removeProductFromOrder),全部内部 mutate tempOrder.products,避免事件风暴;
623
+ * - 用 `isApplyingPromotion` 防递归;
624
+ * - 多选项赠品依赖 giftSelectResolver,未注入则跳过且 console.warn;
625
+ * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进。
626
+ */
627
+ async applyPromotion() {
628
+ if (this.isApplyingPromotion) {
629
+ return;
630
+ }
631
+ if (!this.promotionEvaluator) {
632
+ return;
633
+ }
634
+ const tempOrder = this.store.tempOrder;
635
+ if (!tempOrder) {
636
+ return;
637
+ }
638
+ const strategyConfigs = this.promotionEvaluator.getStrategyConfigs?.();
639
+ if (Array.isArray(strategyConfigs) && strategyConfigs.length === 0) {
640
+ return;
641
+ }
642
+ this.isApplyingPromotion = true;
298
643
  try {
299
- const parsedData = JSON.parse(cachedData);
300
- if (!(0, _utils.isTempOrder)(parsedData)) {
301
- this.window.localStorage.removeItem(key);
302
- return;
303
- }
304
- if (Array.isArray(parsedData.products)) {
305
- for (let i = 0; i < parsedData.products.length; i++) {
306
- const p = parsedData.products[i];
307
- if (!p || typeof p !== 'object') continue;
308
- if (!Array.isArray(p.product_option_item)) {
309
- p.product_option_item = [];
644
+ const result = (0, _cartPromotion.processCartPromotion)(tempOrder.products, this.promotionEvaluator);
645
+
646
+ // step 1: toRemove —— 直接过滤
647
+ if (result.giftActions.toRemove.length > 0) {
648
+ const removeSet = new Set(result.giftActions.toRemove.map(String));
649
+ result.products = result.products.filter(p => {
650
+ const uid = (0, _cartPromotion.getOrderProductUid)(p);
651
+ return uid ? !removeSet.has(String(uid)) : true;
652
+ });
653
+ }
654
+
655
+ // step 2: toReduce —— 调整数量
656
+ for (const reduce of result.giftActions.toReduce) {
657
+ for (const item of reduce.items) {
658
+ const product = result.products.find(p => (0, _cartPromotion.getOrderProductUid)(p) === item.uid);
659
+ if (product) {
660
+ product.num = item.newQuantity;
310
661
  }
311
- if (!Array.isArray(p.product_bundle)) {
312
- p.product_bundle = [];
662
+ }
663
+ }
664
+
665
+ // step 3: toAdd —— 补赠品
666
+ if (result.giftActions.toAdd.length > 0) {
667
+ if (!this.giftSelectResolver) {
668
+ if (result.giftActions.toAdd.some(g => g.giftOptions.length > 1)) {
669
+ // 多选项必须走 resolver;单选项也建议走(避免 OS 自造 stub),但允许跳过
670
+ console.warn('[OrderModule] giftSelectResolver not set, skip applying gifts', result.giftActions.toAdd);
671
+ } else {
672
+ console.warn('[OrderModule] giftSelectResolver not set, skip applying single-option gifts', result.giftActions.toAdd);
313
673
  }
314
- // 不透明 identity 契约:旧缓存里可能没有 identity_key,
315
- // 此处统一回填 UUID 并同步 metadata.unique_identification_number,
316
- // 避免升级后 UI 侧持有的 rowKey 找不到对应行。
317
- const row = p;
318
- if (typeof row.identity_key !== 'string' || row.identity_key.length === 0) {
319
- const newKey = (0, _utils.createUuidV4)();
320
- row.identity_key = newKey;
321
- if (!row.metadata || typeof row.metadata !== 'object') {
322
- row.metadata = {};
674
+ } else {
675
+ for (const addAction of result.giftActions.toAdd) {
676
+ // 单选普通赠品可累加;预约赠品必须新增独立 booking,不能只改 num。
677
+ if (addAction.giftOptions.length === 1) {
678
+ const sameStrategy = result.products.find(p => p.metadata?._giftInfo?.strategyId === addAction.strategyId);
679
+ if (sameStrategy && !sameStrategy.booking_uid && !sameStrategy.metadata?.booking_uid) {
680
+ sameStrategy.num = (Number(sameStrategy.num) || 0) + (addAction.giftCount || 1);
681
+ continue;
682
+ }
323
683
  }
324
- if (!row.metadata.unique_identification_number) {
325
- row.metadata.unique_identification_number = newKey;
684
+ try {
685
+ const giftResult = await this.giftSelectResolver({
686
+ strategyId: addAction.strategyId,
687
+ strategyName: addAction.strategyName,
688
+ giftCount: addAction.giftCount,
689
+ giftOptions: addAction.giftOptions,
690
+ sourceProductIds: addAction.sourceProductIds
691
+ });
692
+ const giftProducts = Array.isArray(giftResult) ? giftResult : giftResult?.products;
693
+ const giftBookings = Array.isArray(giftResult) ? [] : giftResult?.bookings || [];
694
+ if (Array.isArray(giftProducts) && giftProducts.length > 0) {
695
+ for (const [index, gp] of giftProducts.entries()) {
696
+ if (!gp) continue;
697
+ const booking = giftBookings[index];
698
+ if (booking) {
699
+ const linked = this.createLinkedProductAndBooking({
700
+ product: gp,
701
+ booking
702
+ });
703
+ result.products.push(linked.product);
704
+ tempOrder.bookings = [...(tempOrder.bookings || []), linked.booking];
705
+ continue;
706
+ }
707
+ // normalize 略过:调用方应保证返回完整 OrderProduct。
708
+ result.products.push(gp);
709
+ }
710
+ }
711
+ } catch (error) {
712
+ // 用户取消 / 选择失败 → 视为放弃本次新增
713
+ console.warn('[OrderModule] giftSelectResolver rejected, skip add gift', addAction.strategyId, error);
326
714
  }
327
715
  }
328
- // 价格 schema 迁移:重跑 normalizeOrderProduct。
329
- // `metadata.price_schema_version === 2` → 已是新语义,保留现有 main_product_* 折扣;
330
- // 否则(v1 或无 metadata)→ 走 legacyDiscount 反推分支,基于新的 source + options
331
- // 重算出含 option 的 main_product_*,并打上 price_schema_version: 2。
332
- // 幂等:多次 restore 不会重复叠加 option/bundle。
333
- parsedData.products[i] = (0, _utils3.normalizeOrderProduct)(row);
334
716
  }
335
717
  }
336
- this.store.tempOrder = parsedData;
337
- } catch {
338
- this.window?.localStorage?.removeItem(key);
718
+ tempOrder.products = result.products;
719
+ this.lastUnfulfilledPromotions = result.unfulfilledPromotions;
720
+ this.lastGiftActions = result.giftActions;
721
+ const payload = {
722
+ unfulfilledPromotions: result.unfulfilledPromotions,
723
+ giftActions: result.giftActions,
724
+ gifts: result.gifts.map(g => ({
725
+ strategyId: g.strategyId,
726
+ strategyName: g.strategyName,
727
+ giftCount: g.giftCount,
728
+ giftOptions: g.giftOptions
729
+ })),
730
+ products: tempOrder.products
731
+ };
732
+ this.effectsEmit('onPromotionApplied', payload);
733
+ } catch (error) {
734
+ console.error('[OrderModule] applyPromotion failed', error);
735
+ } finally {
736
+ this.isApplyingPromotion = false;
339
737
  }
340
738
  }
739
+
740
+ /** 最近一次 applyPromotion 输出的未满足促销列表 */
741
+ getUnfulfilledPromotions() {
742
+ return this.lastUnfulfilledPromotions;
743
+ }
744
+
745
+ /** 最近一次 applyPromotion 输出的赠品操作 diff */
746
+ getLastGiftActions() {
747
+ return this.lastGiftActions;
748
+ }
749
+
750
+ // ─── TempOrder: 初始化入口 ───
751
+
752
+ initTempOrder(params) {
753
+ if (params.cacheId !== undefined) this.cacheId = params.cacheId;
754
+ if (params.salesSummaryModuleName !== undefined) this.salesSummaryModuleName = params.salesSummaryModuleName;
755
+ }
756
+
757
+ /**
758
+ * 控制 IndexedDB 草稿(saveDraft)是否写入。
759
+ * localStorage 版 tempOrder 持久化已废弃,此方法仅影响 saveDraft。
760
+ *
761
+ * @example
762
+ * order.setEnableTempOrderPersist(false); // BigSale 弹窗:跳过 ~1s 的 dbUpdate
763
+ */
764
+ setEnableTempOrderPersist(enabled) {
765
+ this.draftPersistEnabled = enabled;
766
+ }
767
+
768
+ /** 是否允许 saveDraft 写入 IndexedDB 草稿。 */
769
+ isTempOrderPersistEnabled() {
770
+ return this.draftPersistEnabled;
771
+ }
772
+
773
+ /** @deprecated OrderModule 不再负责 tempOrder localStorage 持久化;草稿保存请使用 IndexedDB saveDraft。 */
341
774
  persistTempOrder() {
342
- const key = this.getTempOrderStorageKey();
343
- if (!key || !this.store.tempOrder) return;
344
- if (!this.window) return;
345
- this.window.localStorage.setItem(key, JSON.stringify(this.store.tempOrder));
775
+ // no-op: OrderModule 的刷新恢复不再依赖 localStorage。
346
776
  }
347
777
  notifyTempOrderChanged() {
348
778
  if (!this.store.tempOrder) return;
@@ -358,263 +788,2722 @@ class OrderModule extends _BaseModule.BaseModule {
358
788
 
359
789
  createDefaultTempOrderInstance() {
360
790
  return (0, _utils.createDefaultTempOrder)({
361
- now: (0, _utils.formatDateTime)(new Date()),
362
- summary: (0, _utils.createEmptySummary)()
791
+ now: (0, _utils.formatDateTime)(new Date())
363
792
  });
364
793
  }
365
- ensureTempOrder() {
366
- if (this.store.tempOrder) return this.store.tempOrder;
367
- const newOrder = this.createDefaultTempOrderInstance();
368
- this.store.tempOrder = newOrder;
369
- this.persistTempOrder();
370
- return newOrder;
794
+ createExternalSaleNumber() {
795
+ const timestamp = (0, _dayjs.default)().format('YYYYMMDDHHmmssSSS');
796
+ const random6 = String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
797
+ const parts = ['LOCAL'];
798
+ const context = this.core?.context;
799
+ const getData = context?.getData;
800
+ if (typeof getData === 'function') {
801
+ const shopId = getData.call(context, 'shop_id');
802
+ const deviceId = getData.call(context, 'device_id');
803
+ if (shopId !== undefined && shopId !== null && shopId !== '') {
804
+ parts.push(String(shopId));
805
+ }
806
+ if (deviceId !== undefined && deviceId !== null && deviceId !== '') {
807
+ parts.push(String(deviceId).slice(-8));
808
+ }
809
+ } else {
810
+ this.logWarning('external_sale_number context getData unavailable', {
811
+ key: 'shop_id'
812
+ });
813
+ this.logWarning('external_sale_number context getData unavailable', {
814
+ key: 'device_id'
815
+ });
816
+ }
817
+ parts.push(timestamp, random6);
818
+ return parts.join('_');
371
819
  }
372
- restoreOrder() {
373
- const freshTempOrder = this.createDefaultTempOrderInstance();
374
- this.store.tempOrder = freshTempOrder;
375
- this.persistTempOrder();
376
- return freshTempOrder;
820
+ ensureExternalSaleNumber(tempOrder) {
821
+ if (!tempOrder.external_sale_number) {
822
+ tempOrder.external_sale_number = this.createExternalSaleNumber();
823
+ }
824
+ this.ensureRequestUniqueIdempotencyToken(tempOrder);
825
+ return tempOrder.external_sale_number;
377
826
  }
378
- getTempOrder() {
379
- return this.store.tempOrder;
827
+ ensureRequestUniqueIdempotencyToken(tempOrder) {
828
+ if (!tempOrder.request_unique_idempotency_token) {
829
+ tempOrder.request_unique_idempotency_token = `${tempOrder.external_sale_number}_${Date.now()}`;
830
+ }
831
+ return tempOrder.request_unique_idempotency_token;
380
832
  }
381
- async addNewOrder() {
833
+ buildPaymentSyncIdempotencyToken(tempOrder, payments) {
834
+ const baseToken = this.ensureRequestUniqueIdempotencyToken(tempOrder);
835
+ const paidPaymentsCount = (payments || []).filter(payment => {
836
+ return String(payment?.status || '').toLowerCase() === 'paid';
837
+ }).length;
838
+ const paidCount = paidPaymentsCount > 0 ? paidPaymentsCount : payments.length;
839
+ return `${baseToken}_payment_${paidCount}`;
840
+ }
841
+ async saveDraft() {
842
+ // no-op: 普通 OrderModule 不再负责订单草稿或本地数据库持久化。
843
+ }
844
+ saveDraftInBackground() {
845
+ void this.saveDraft();
846
+ }
847
+ async hydrateTempOrderFromLocalRecord(record) {
848
+ return this.hydrateTempOrderFromRecord(record);
849
+ }
850
+ extractOrderIdFromSubmitResult(result) {
851
+ return result?.data?.order_id ?? result?.order_id ?? result?.data?.id ?? result?.id ?? null;
852
+ }
853
+ calculatePaymentEffectiveAmount(payment) {
854
+ const amount = new _decimal.default(payment.amount || 0);
855
+ const originAmount = new _decimal.default(payment.origin_amount ?? payment.amount ?? 0);
856
+ const serviceFee = new _decimal.default(payment.service_fee || 0);
857
+ // TODO: 后端允许 amount 表示顾客实际支付总额后,移除这个兼容分支。
858
+ const effectiveAmount = serviceFee.gt(0) && amount.eq(originAmount) ? amount.plus(serviceFee) : amount;
859
+ const rounding = new _decimal.default(payment.rounding_amount || 0);
860
+ return effectiveAmount.minus(rounding);
861
+ }
862
+ calculatePaymentTotal(payments) {
863
+ return (0, _utils.mapPaymentItemsToOrderPayments)(payments, {
864
+ includeVoided: false
865
+ }).reduce((sum, payment) => {
866
+ if (!isPaidPaymentRecord(payment)) return sum;
867
+ return sum.plus(this.calculatePaymentEffectiveAmount(payment));
868
+ }, new _decimal.default(0));
869
+ }
870
+ calculateOrderPaidPaymentTotal(payments) {
871
+ return (0, _utils.mapPaymentItemsToOrderPayments)(payments, {
872
+ includeVoided: false
873
+ }).reduce((sum, payment) => {
874
+ if (!isPaidPaymentRecord(payment)) return sum;
875
+ return sum.plus(this.calculatePaymentGapAmount(payment));
876
+ }, new _decimal.default(0));
877
+ }
878
+ getDepositAmount(tempOrder, summary) {
879
+ return new _decimal.default(summary.deposit_amount || tempOrder.deposit_amount || '0.00');
880
+ }
881
+ normalizeDepositAmount(amount) {
882
+ return _decimal.default.max(new _decimal.default(Number(amount) || 0), 0).toDecimalPlaces(2).toFixed(2);
883
+ }
884
+ getLastOrderDepositSnapshot() {
885
+ const lastOrderInfo = this.store.lastOrderInfo;
886
+ if (!lastOrderInfo || typeof lastOrderInfo !== 'object') return null;
887
+ const rawDepositAmount = lastOrderInfo.deposit_amount ?? lastOrderInfo.summary?.deposit_amount;
888
+ const depositAmount = this.normalizeDepositAmount(rawDepositAmount ?? 0);
889
+ const hasDepositAmount = new _decimal.default(depositAmount).gt(0);
890
+ const isDeposit = Number(lastOrderInfo.is_deposit) === 1 || hasDepositAmount;
891
+ if (!isDeposit || !hasDepositAmount) return null;
892
+ return {
893
+ isDeposit: 1,
894
+ depositAmount
895
+ };
896
+ }
897
+ getLastOrderSubmitSnapshot() {
898
+ const lastOrderInfo = this.store.lastOrderInfo;
899
+ if (!lastOrderInfo || typeof lastOrderInfo !== 'object') return {};
900
+ const businessCode = lastOrderInfo.business_code;
901
+ const type = lastOrderInfo.type;
902
+ return {
903
+ ...(typeof businessCode === 'string' && businessCode ? {
904
+ businessCode
905
+ } : {}),
906
+ ...(typeof type === 'string' && type ? {
907
+ type
908
+ } : {})
909
+ };
910
+ }
911
+ applyDepositAmountToSummary(tempOrder, summary, amount) {
912
+ const depositAmount = this.normalizeDepositAmount(amount);
913
+ const hasDepositAmount = new _decimal.default(depositAmount).gt(0);
914
+ const existedDeposit = summary.deposit && typeof summary.deposit === 'object' ? summary.deposit : undefined;
915
+ const nextSummary = {
916
+ ...summary,
917
+ deposit_amount: depositAmount,
918
+ deposit: hasDepositAmount ? {
919
+ ...(existedDeposit || {}),
920
+ total: depositAmount,
921
+ deposit_policy_ids: existedDeposit?.deposit_policy_ids || [],
922
+ hasDeposit: true
923
+ } : undefined
924
+ };
925
+ tempOrder.deposit_amount = depositAmount;
926
+ tempOrder.is_deposit = hasDepositAmount ? 1 : 0;
927
+ return nextSummary;
928
+ }
929
+ applyLastOrderDepositSnapshot(tempOrder, summary) {
930
+ const snapshot = this.getLastOrderDepositSnapshot();
931
+ if (!snapshot) return summary;
932
+ const existedDeposit = summary.deposit && typeof summary.deposit === 'object' ? summary.deposit : undefined;
933
+ const nextSummary = {
934
+ ...summary,
935
+ deposit_amount: snapshot.depositAmount,
936
+ deposit: {
937
+ ...(existedDeposit || {}),
938
+ total: snapshot.depositAmount,
939
+ deposit_policy_ids: existedDeposit?.deposit_policy_ids || [],
940
+ hasDeposit: true
941
+ }
942
+ };
943
+ tempOrder.deposit_amount = snapshot.depositAmount;
944
+ tempOrder.is_deposit = tempOrder.is_deposit === 0 && new _decimal.default(tempOrder.deposit_amount || 0).gt(0) ? 0 : snapshot.isDeposit;
945
+ return nextSummary;
946
+ }
947
+ isDepositOrder(tempOrder, depositAmount) {
948
+ return tempOrder.is_deposit === 1 && depositAmount.gt(0);
949
+ }
950
+ getOrderExpectedAmount(summary) {
951
+ return new _decimal.default(summary.expect_amount || summary.total_amount || 0);
952
+ }
953
+ isDepositCovered(params) {
954
+ const depositAmount = this.getDepositAmount(params.tempOrder, params.summary);
955
+ if (!this.isDepositOrder(params.tempOrder, depositAmount)) return false;
956
+ if (depositAmount.lte(0)) return false;
957
+ return params.paidAmount.gte(depositAmount);
958
+ }
959
+ getPaymentCompletion(payments) {
382
960
  const tempOrder = this.ensureTempOrder();
383
- await this.recalculateSummary({
384
- createIfMissing: true
961
+ const summary = this.store.summary || (0, _utils.createEmptySummary)();
962
+ const paymentList = payments || tempOrder.payments || [];
963
+ const refundAmount = this.getSummaryRefundAmount(summary);
964
+ const netPaidAmount = _decimal.default.max(this.calculateOrderPaidPaymentTotal(paymentList).minus(refundAmount), 0);
965
+ const depositAmount = this.getDepositAmount(tempOrder, summary);
966
+ const orderExpectedAmount = this.getOrderExpectedAmount(summary);
967
+ const isDepositFullyPaid = this.isDepositCovered({
968
+ tempOrder,
969
+ summary,
970
+ paidAmount: netPaidAmount
385
971
  });
386
- this.persistTempOrder();
387
- return tempOrder;
972
+ const isOrderFullyPaid = orderExpectedAmount.lte(0) ? true : netPaidAmount.gte(orderExpectedAmount);
973
+ const paymentStage = this.isDepositOrder(tempOrder, depositAmount) && !isDepositFullyPaid ? 'deposit' : 'normal';
974
+ return {
975
+ isFullyPaid: isOrderFullyPaid,
976
+ isOrderFullyPaid,
977
+ isDepositFullyPaid,
978
+ paymentStage,
979
+ depositAmount: depositAmount.toFixed(2),
980
+ orderExpectedAmount: orderExpectedAmount.toFixed(2)
981
+ };
388
982
  }
389
- getOrderProducts() {
390
- const tempOrder = this.ensureTempOrder();
391
- return tempOrder.products;
983
+ calculateDepositPaidAmount(payments) {
984
+ return (payments || []).reduce((sum, payment) => {
985
+ const paymentRecord = payment;
986
+ if (!isPaidPaymentRecord(paymentRecord)) return sum;
987
+ const isDepositPayment = paymentRecord.type === 'deposit' || paymentRecord.order_payment_type === 'deposit';
988
+ if (!isDepositPayment) return sum;
989
+ return sum.plus(this.calculatePaymentGapAmount(paymentRecord));
990
+ }, new _decimal.default(0));
392
991
  }
393
-
394
- // ─── TempOrder: 金额汇总 ───
395
-
396
- getSalesSummary() {
397
- if (!this.salesSummaryModuleName) return null;
398
- return this.core.getModule(this.salesSummaryModuleName);
992
+ resolveNextOrderPaymentType(tempOrder) {
993
+ const depositAmount = new _decimal.default(tempOrder.deposit_amount || '0.00');
994
+ if (!this.isDepositOrder(tempOrder, depositAmount)) return 'normal';
995
+ if (depositAmount.lte(0)) return 'normal';
996
+ const depositPaidAmount = this.calculateDepositPaidAmount(tempOrder.payments || []);
997
+ return depositPaidAmount.lt(depositAmount) ? 'deposit' : 'normal';
399
998
  }
400
- async recalculateSummary(options) {
401
- const tempOrder = options?.createIfMissing ? this.ensureTempOrder() : this.store.tempOrder;
402
- if (!tempOrder) return null;
403
- const salesSummary = this.getSalesSummary();
404
- if (!salesSummary) {
405
- tempOrder.summary = (0, _utils.createEmptySummary)();
406
- tempOrder.surcharge_fee = tempOrder.summary.surcharge_fee;
407
- return tempOrder.summary;
999
+ normalizePaymentSource(payment, options) {
1000
+ const paymentRecord = payment;
1001
+ const metadata = paymentRecord.metadata && typeof paymentRecord.metadata === 'object' ? {
1002
+ ...paymentRecord.metadata
1003
+ } : {};
1004
+ if (!metadata.unique_payment_number) {
1005
+ metadata.unique_payment_number = (0, _utils.createUuidV4)();
408
1006
  }
409
- const summary = await salesSummary.getSummary({
410
- products: tempOrder.products
411
- });
412
- tempOrder.summary = summary;
413
- if (summary.is_price_include_tax !== undefined) {
414
- tempOrder.is_price_include_tax = summary.is_price_include_tax;
1007
+ const normalized = {
1008
+ ...paymentRecord,
1009
+ metadata,
1010
+ origin_amount: paymentRecord.origin_amount ?? paymentRecord.amount ?? '0.00'
1011
+ };
1012
+ if (paymentRecord.status !== undefined) {
1013
+ normalized.status = paymentRecord.status;
1014
+ } else if (options?.defaultPaid !== false) {
1015
+ normalized.status = 'paid';
415
1016
  }
416
- if (typeof summary.tax_title === 'string') {
417
- tempOrder.tax_title = summary.tax_title;
1017
+ return normalized;
1018
+ }
1019
+ updateTempOrderPaymentStatus(tempOrder, options) {
1020
+ const summary = options?.summary || this.store.summary || (0, _utils.createEmptySummary)();
1021
+ const paymentTotal = this.calculatePaymentTotal(tempOrder.payments || []);
1022
+ const orderPaymentTotal = this.calculateOrderPaidPaymentTotal(tempOrder.payments || []);
1023
+ const refundAmount = this.getSummaryRefundAmount(summary);
1024
+ const netPaymentTotal = _decimal.default.max(orderPaymentTotal.minus(refundAmount), 0);
1025
+ const targetAmount = this.calculatePaymentTargetAmount(tempOrder, summary, netPaymentTotal);
1026
+ if (this.shouldKeepPaymentStatusAfterAmountRecalc(tempOrder.payment_status, {
1027
+ netPaymentTotal,
1028
+ paymentTotal,
1029
+ refundAmount,
1030
+ targetAmount,
1031
+ previousSummary: options?.previousSummary,
1032
+ tempOrder
1033
+ })) {
1034
+ return;
418
1035
  }
419
- tempOrder.surcharge_fee = summary.surcharge_fee;
420
- tempOrder.surcharges = summary.surcharges || [];
421
- tempOrder.deposit_amount = summary.deposit_amount || '0.00';
422
- tempOrder.is_deposit = summary.deposit_amount !== '0.00' ? 1 : 0;
423
- tempOrder.shop_discount = summary.discount_amount || '0.00';
424
- return tempOrder.summary;
1036
+ if (targetAmount.lte(0)) {
1037
+ tempOrder.payment_status = 'paid';
1038
+ return;
1039
+ }
1040
+ if (netPaymentTotal.lte(0)) {
1041
+ tempOrder.payment_status = 'payment_processing';
1042
+ return;
1043
+ }
1044
+ tempOrder.payment_status = netPaymentTotal.gte(targetAmount) ? 'paid' : 'partially_paid';
425
1045
  }
426
- async getScanOrderSummary() {
427
- const summary = await this.recalculateSummary({
428
- createIfMissing: true
1046
+ shouldKeepPaymentStatusAfterAmountRecalc(paymentStatus, params) {
1047
+ const normalizedStatus = String(paymentStatus || '').trim().toLowerCase();
1048
+ if (normalizedStatus !== 'refunded' && normalizedStatus !== 'partially_refunded') {
1049
+ return false;
1050
+ }
1051
+ if (normalizedStatus === 'refunded' && params.paymentTotal.gt(0) && params.refundAmount.gte(params.paymentTotal)) {
1052
+ return true;
1053
+ }
1054
+ if (params.previousSummary) {
1055
+ const previousTargetAmount = this.calculatePaymentTargetAmount(params.tempOrder, params.previousSummary, params.netPaymentTotal);
1056
+ if (!previousTargetAmount.eq(params.targetAmount)) return false;
1057
+ }
1058
+ return true;
1059
+ }
1060
+ resolveWalletPassUseValue(payment, amount) {
1061
+ const usageUnit = payment.wallet_pass_usage_unit && typeof payment.wallet_pass_usage_unit === 'object' ? payment.wallet_pass_usage_unit : null;
1062
+ if (!usageUnit) return payment.wallet_pass_use_value;
1063
+ const price = new _decimal.default(usageUnit.price || 0);
1064
+ const points = new _decimal.default(usageUnit.points || 0);
1065
+ if (price.lte(0) || points.lte(0)) return payment.wallet_pass_use_value;
1066
+ return amount.div(price).times(points).toDecimalPlaces(2).toFixed(2);
1067
+ }
1068
+ adjustVoucherFundRecord(params) {
1069
+ const fundRecord = params.voucher.fund_record && typeof params.voucher.fund_record === 'object' && !Array.isArray(params.voucher.fund_record) ? {
1070
+ ...params.voucher.fund_record
1071
+ } : null;
1072
+ if (!fundRecord || fundRecord.using_before_value === undefined || fundRecord.using_before_value === null) {
1073
+ return fundRecord || undefined;
1074
+ }
1075
+ const code = String(params.voucher.code || '').toUpperCase();
1076
+ const isPointCard = code === 'POINTCARD' || String(params.voucher.fund_record?.tag || '').toLowerCase() === 'point_card';
1077
+ const usingValue = isPointCard ? new _decimal.default(params.walletPassUseValue || 0) : params.amount;
1078
+ fundRecord.using_after_value = new _decimal.default(fundRecord.using_before_value || 0).minus(usingValue).toDecimalPlaces(2).toFixed(2);
1079
+ return fundRecord;
1080
+ }
1081
+ capVoucherPaymentsByRemainingAmount(params) {
1082
+ const targetAmount = this.getPaymentTargetAmount();
1083
+ const nonVoucherTotal = this.calculateOrderPaidPaymentTotal(params.nonVoucherPayments);
1084
+ if (targetAmount.lte(0)) return [];
1085
+ let remainingAmount = _decimal.default.max(targetAmount.minus(nonVoucherTotal), 0);
1086
+ const cappedVouchers = [];
1087
+ for (const voucher of params.vouchers) {
1088
+ if (remainingAmount.lte(0)) break;
1089
+ const voucherAmount = _decimal.default.max(new _decimal.default(voucher.amount || 0), 0);
1090
+ if (voucherAmount.lte(0)) continue;
1091
+ const cappedAmount = _decimal.default.min(voucherAmount, remainingAmount);
1092
+ const amountText = cappedAmount.toDecimalPlaces(2).toFixed(2);
1093
+ const walletPassUseValue = this.resolveWalletPassUseValue(voucher, cappedAmount);
1094
+ cappedVouchers.push({
1095
+ ...voucher,
1096
+ amount: amountText,
1097
+ origin_amount: amountText,
1098
+ wallet_pass_use_value: walletPassUseValue,
1099
+ fund_record: this.adjustVoucherFundRecord({
1100
+ voucher: voucher,
1101
+ amount: cappedAmount,
1102
+ walletPassUseValue
1103
+ })
1104
+ });
1105
+ remainingAmount = remainingAmount.minus(cappedAmount);
1106
+ }
1107
+ return cappedVouchers;
1108
+ }
1109
+ calculatePaidPaymentSummary(payments) {
1110
+ return (payments || []).reduce((summary, payment) => {
1111
+ const paymentRecord = payment;
1112
+ if (!isPaidPaymentRecord(paymentRecord)) return summary;
1113
+ return {
1114
+ customerPaidAmount: summary.customerPaidAmount.plus(paymentRecord.amount || 0),
1115
+ orderPaidAmount: summary.orderPaidAmount.plus(this.calculatePaymentOrderAmount(paymentRecord)),
1116
+ gapPaidAmount: summary.gapPaidAmount.plus(this.calculatePaymentGapAmount(paymentRecord)),
1117
+ payServiceChargeAmount: summary.payServiceChargeAmount.plus(this.calculatePaymentServiceFee(paymentRecord)),
1118
+ roundingAmount: summary.roundingAmount.plus(this.calculatePaymentRoundingAmount(paymentRecord))
1119
+ };
1120
+ }, {
1121
+ customerPaidAmount: new _decimal.default(0),
1122
+ orderPaidAmount: new _decimal.default(0),
1123
+ gapPaidAmount: new _decimal.default(0),
1124
+ payServiceChargeAmount: new _decimal.default(0),
1125
+ roundingAmount: new _decimal.default(0)
429
1126
  });
430
- if (!summary) return (0, _utils.createEmptySummary)();
431
- this.persistTempOrder();
432
- return summary;
433
1127
  }
434
-
435
- // ─── TempOrder: 备注 ───
436
-
437
- updateTempOrderNote(note) {
438
- const tempOrder = this.ensureTempOrder();
439
- tempOrder.note = String(note || '');
440
- this.persistTempOrder();
441
- return tempOrder.note;
1128
+ calculatePaymentServiceFee(payment) {
1129
+ if (payment.service_fee !== undefined && payment.service_fee !== null && payment.service_fee !== '') {
1130
+ return new _decimal.default(payment.service_fee || 0);
1131
+ }
1132
+ const serviceCharge = payment.service_charge && typeof payment.service_charge === 'object' ? payment.service_charge : {};
1133
+ const percentage = new _decimal.default(serviceCharge.percentage || 0);
1134
+ const fixedAmount = new _decimal.default(serviceCharge.amount || 0);
1135
+ return new _decimal.default(payment.amount || 0).times(percentage).plus(fixedAmount);
442
1136
  }
443
- updateTempOrderBuzzer(buzzer) {
444
- const tempOrder = this.ensureTempOrder();
445
- tempOrder.buzzer = String(buzzer || '');
446
- this.persistTempOrder();
447
- return tempOrder.buzzer;
1137
+ calculatePaymentGapAmount(payment) {
1138
+ if (payment.origin_amount !== undefined && payment.origin_amount !== null && payment.origin_amount !== '') {
1139
+ const rounding = new _decimal.default(payment.rounding_amount || 0);
1140
+ return new _decimal.default(payment.origin_amount || 0).minus(rounding);
1141
+ }
1142
+ const rounding = new _decimal.default(payment.rounding_amount || 0);
1143
+ const amount = new _decimal.default(payment.amount || 0);
1144
+ const serviceFee = this.calculatePaymentServiceFee(payment);
1145
+ if (serviceFee.lte(0)) return amount.minus(rounding);
1146
+ if (!hasSyncedOrderPaymentRecord(payment)) return new _decimal.default(0);
1147
+ return _decimal.default.max(amount.minus(serviceFee), 0).minus(rounding);
448
1148
  }
449
- updateTempOrderContactsInfo(contactsInfo) {
450
- const tempOrder = this.ensureTempOrder();
451
- tempOrder.contacts_info = Array.isArray(contactsInfo) ? contactsInfo : [];
452
- this.persistTempOrder();
453
- return tempOrder.contacts_info;
1149
+ calculatePaymentOrderAmount(payment) {
1150
+ if (payment.origin_amount !== undefined && payment.origin_amount !== null && payment.origin_amount !== '') {
1151
+ return new _decimal.default(payment.origin_amount || 0);
1152
+ }
1153
+ const amount = new _decimal.default(payment.amount || 0);
1154
+ const serviceFee = this.calculatePaymentServiceFee(payment);
1155
+ if (serviceFee.lte(0)) return amount;
1156
+ if (!hasSyncedOrderPaymentRecord(payment)) return new _decimal.default(0);
1157
+ return _decimal.default.max(amount.minus(serviceFee), 0);
454
1158
  }
455
-
456
- // ─── TempOrder: 商品 CRUD ───
457
-
458
- async addProductToOrder(product) {
459
- const tempOrder = this.ensureTempOrder();
460
- const hasExplicitIdentityKey = typeof product.identity_key === 'string' && product.identity_key.length > 0;
461
- if (hasExplicitIdentityKey) {
462
- // 调用方自持 identity_key(如 VenueBooking 外部生成)→ 走严格 identity 合并。
463
- const normalizedProduct = (0, _utils3.normalizeOrderProduct)(product);
464
- const productIndex = (0, _utils3.getProductIdentityIndex)(tempOrder.products, normalizedProduct);
465
- if (productIndex === -1) {
466
- tempOrder.products.push(normalizedProduct);
467
- } else {
468
- const targetProduct = tempOrder.products[productIndex];
469
- tempOrder.products[productIndex] = {
470
- ...targetProduct,
471
- ...normalizedProduct,
472
- num: (0, _utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num),
473
- _origin: normalizedProduct._origin || targetProduct._origin
474
- };
1159
+ calculatePaymentRoundingAmount(payment) {
1160
+ if (payment.rounding_amount === undefined || payment.rounding_amount === null || payment.rounding_amount === '') {
1161
+ return new _decimal.default(0);
1162
+ }
1163
+ return new _decimal.default(payment.rounding_amount || 0);
1164
+ }
1165
+ formatSummaryMetadataMoney(value) {
1166
+ if (value === undefined || value === null || value === '') return undefined;
1167
+ if (value instanceof _decimal.default) return value.toFixed(2);
1168
+ return new _decimal.default(String(value || 0)).toFixed(2);
1169
+ }
1170
+ syncSummaryProductMetadata(products) {
1171
+ const moneyKeys = ['main_product_attached_bundle_surcharge_fee', 'main_product_attached_bundle_tax_fee', 'surcharge_rounding_remainder', 'tax_fee_rounding_remainder'];
1172
+ // 整单折扣均摊层产出的 payment 维度字段,直接由计算层写入 metadata,
1173
+ // 这里需把它们一并回写到商品 metadata 以便持久化/出站。
1174
+ const metadataMoneyKeys = ['main_product_attached_bundle_selling_price', 'main_product_attached_bundle_payment_price'];
1175
+ for (const product of products || []) {
1176
+ const productRecord = product;
1177
+ const metadata = productRecord.metadata && typeof productRecord.metadata === 'object' ? {
1178
+ ...productRecord.metadata
1179
+ } : {};
1180
+ let hasMetadataPatch = false;
1181
+ for (const key of moneyKeys) {
1182
+ const value = this.formatSummaryMetadataMoney(productRecord[key]);
1183
+ if (value === undefined) continue;
1184
+ metadata[key] = value;
1185
+ hasMetadataPatch = true;
475
1186
  }
476
- } else {
477
- // 无 identity_key:按 SKU + 内容指纹(options + bundle)匹配既有行以累加数量;
478
- // 命中时保留已有 identity_key,未命中才由 normalizeOrderProduct 自动生成 UUID。
479
- const incomingFingerprint = (0, _utils3.buildProductLineFingerprint)(product.product_option_item, product.product_bundle);
480
- const matchedIndex = tempOrder.products.findIndex(item => {
481
- if (item.product_id !== product.product_id) return false;
482
- if (item.product_variant_id !== product.product_variant_id) return false;
483
- const existedFingerprint = (0, _utils3.buildProductLineFingerprint)(item.product_option_item, item.product_bundle);
484
- return existedFingerprint === incomingFingerprint;
485
- });
486
- if (matchedIndex === -1) {
487
- const normalizedProduct = (0, _utils3.normalizeOrderProduct)(product);
488
- tempOrder.products.push(normalizedProduct);
489
- } else {
490
- const targetProduct = tempOrder.products[matchedIndex];
491
- const normalizedProduct = (0, _utils3.normalizeOrderProduct)({
492
- ...product,
493
- identity_key: targetProduct.identity_key
494
- });
495
- tempOrder.products[matchedIndex] = {
496
- ...targetProduct,
497
- ...normalizedProduct,
498
- identity_key: targetProduct.identity_key,
499
- num: (0, _utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num),
500
- _origin: normalizedProduct._origin || targetProduct._origin
501
- };
1187
+ for (const key of metadataMoneyKeys) {
1188
+ const value = this.formatSummaryMetadataMoney(metadata[key]);
1189
+ if (value === undefined) continue;
1190
+ metadata[key] = value;
1191
+ hasMetadataPatch = true;
1192
+ }
1193
+ if (metadata.average_discount_amount_rate !== undefined) {
1194
+ hasMetadataPatch = true;
1195
+ }
1196
+ if (Array.isArray(productRecord.relation_surcharge_ids)) {
1197
+ metadata.relation_surcharge_ids = productRecord.relation_surcharge_ids;
1198
+ hasMetadataPatch = true;
1199
+ }
1200
+ const productPaymentPrice = this.formatSummaryMetadataMoney(productRecord.payment_price);
1201
+ if (productPaymentPrice !== undefined) {
1202
+ productRecord.payment_price = productPaymentPrice;
1203
+ }
1204
+ if (hasMetadataPatch) productRecord.metadata = metadata;
1205
+ if (!Array.isArray(productRecord.product_bundle)) continue;
1206
+ for (const bundle of productRecord.product_bundle) {
1207
+ const bundleRecord = bundle;
1208
+ const bundleMetadata = bundleRecord.metadata && typeof bundleRecord.metadata === 'object' ? {
1209
+ ...bundleRecord.metadata
1210
+ } : {};
1211
+ let hasBundleMetadataPatch = false;
1212
+ const bundleSurchargeFee = this.formatSummaryMetadataMoney(bundleRecord.surcharge_fee);
1213
+ if (bundleSurchargeFee !== undefined) {
1214
+ bundleMetadata.surcharge_fee = bundleSurchargeFee;
1215
+ hasBundleMetadataPatch = true;
1216
+ }
1217
+ const bundlePaymentPrice = this.formatSummaryMetadataMoney(bundleRecord.bundle_payment_price);
1218
+ if (bundlePaymentPrice !== undefined) {
1219
+ bundleRecord.bundle_payment_price = bundlePaymentPrice;
1220
+ }
1221
+ if (Array.isArray(bundleRecord.relation_surcharge_ids)) {
1222
+ bundleMetadata.relation_surcharge_ids = bundleRecord.relation_surcharge_ids;
1223
+ hasBundleMetadataPatch = true;
1224
+ }
1225
+ if (hasBundleMetadataPatch) bundleRecord.metadata = bundleMetadata;
502
1226
  }
503
1227
  }
504
- this.applyDiscount();
505
- await this.recalculateSummary({
506
- createIfMissing: true
507
- });
508
- this.persistTempOrder();
509
- return tempOrder.products;
510
1228
  }
511
- async updateProductInOrder(params) {
1229
+ calculateSummaryAmountGap(params) {
512
1230
  const {
513
- product_id,
514
- product_variant_id,
515
- updates,
516
- identity_key,
517
- product_option_item,
518
- product_bundle
1231
+ tempOrder,
1232
+ summary,
1233
+ orderPaidAmount
519
1234
  } = params;
520
- const tempOrder = this.ensureTempOrder();
1235
+ const refundAmount = this.getSummaryRefundAmount(summary);
1236
+ const netOrderPaidAmount = _decimal.default.max(orderPaidAmount.minus(refundAmount), 0);
1237
+ const targetAmount = this.calculatePaymentTargetAmount(tempOrder, summary, netOrderPaidAmount);
1238
+ return targetAmount.minus(netOrderPaidAmount).toFixed(2);
1239
+ }
1240
+ getSummaryRefundAmount(summary) {
1241
+ return _decimal.default.max(new _decimal.default(summary?.total_refund_amount || 0), 0);
1242
+ }
1243
+ calculatePaymentTargetAmount(tempOrder, summary, paidAmount = new _decimal.default(0)) {
1244
+ const depositAmount = this.getDepositAmount(tempOrder, summary);
1245
+ if (!this.isDepositOrder(tempOrder, depositAmount)) {
1246
+ return this.getOrderExpectedAmount(summary);
1247
+ }
1248
+ if (this.isDepositCovered({
1249
+ tempOrder,
1250
+ summary,
1251
+ paidAmount
1252
+ })) {
1253
+ return this.getOrderExpectedAmount(summary);
1254
+ }
1255
+ return depositAmount;
1256
+ }
1257
+ normalizeFingerprintMoney(value) {
1258
+ if (value === undefined || value === null || value === '') return '';
1259
+ try {
1260
+ const parsed = new _decimal.default(String(value || 0));
1261
+ return parsed.toFixed(4);
1262
+ } catch {
1263
+ return String(value);
1264
+ }
1265
+ }
1266
+ normalizeFingerprintValue(value) {
1267
+ if (Array.isArray(value)) {
1268
+ return value.map(item => this.normalizeFingerprintValue(item));
1269
+ }
1270
+ if (!value || typeof value !== 'object') return value ?? '';
1271
+ return Object.keys(value).sort().reduce((result, key) => {
1272
+ result[key] = this.normalizeFingerprintValue(value[key]);
1273
+ return result;
1274
+ }, {});
1275
+ }
1276
+ buildProductFingerprintItem(product) {
1277
+ const sku = product?.product_sku || {};
1278
+ const metadata = product?.metadata || {};
1279
+ const discountTypes = (product?.discount_list || []).map(discount => discount?.type ?? discount?.tag);
1280
+ const extensionType = product?.extension_type || (discountTypes.includes('good_pass') ? 'good_pass' : 'normal');
1281
+ return {
1282
+ product_id: product?.product_id ?? null,
1283
+ product_variant_id: product?.product_variant_id ?? null,
1284
+ extension_type: extensionType,
1285
+ num: (0, _utils3.getSafeProductNum)(product?.num ?? product?.product_quantity),
1286
+ selling_price: this.normalizeFingerprintMoney(product?.selling_price),
1287
+ original_price: this.normalizeFingerprintMoney(product?.original_price),
1288
+ payment_price: this.normalizeFingerprintMoney(product?.payment_price ?? product?.selling_price),
1289
+ metadata: {
1290
+ source_product_price: this.normalizeFingerprintMoney(metadata.source_product_price),
1291
+ main_product_selling_price: this.normalizeFingerprintMoney(metadata.main_product_selling_price),
1292
+ main_product_original_price: this.normalizeFingerprintMoney(metadata.main_product_original_price),
1293
+ product_discount_difference: this.normalizeFingerprintMoney(metadata.product_discount_difference)
1294
+ },
1295
+ product_sku: this.normalizeFingerprintValue({
1296
+ code: sku.code ?? '',
1297
+ barcode: sku.barcode ?? '',
1298
+ variant_id: sku.variant_id ?? product?.product_variant_id ?? 0,
1299
+ variant: sku.variant || [],
1300
+ option: (0, _utils.getProductSkuOptions)(product || {}).map(option => ({
1301
+ id: option?.id ?? option?.option_group_item_id ?? null,
1302
+ option_group_id: option?.option_group_id ?? null,
1303
+ option_group_item_id: option?.option_group_item_id ?? option?.id ?? null,
1304
+ num: Number(option?.num ?? 1) || 1,
1305
+ add_price: this.normalizeFingerprintMoney(option?.add_price ?? option?.price)
1306
+ }))
1307
+ }),
1308
+ product_bundle: this.normalizeFingerprintValue((product?.product_bundle || []).map(bundle => ({
1309
+ bundle_product_id: bundle?.bundle_product_id ?? bundle?._bundle_product_id ?? bundle?.product_id ?? null,
1310
+ bundle_variant_id: bundle?.bundle_variant_id ?? bundle?.product_variant_id ?? 0,
1311
+ num: (0, _utils3.getSafeProductNum)(bundle?.num ?? bundle?.quantity),
1312
+ price: this.normalizeFingerprintMoney(bundle?.price),
1313
+ bundle_selling_price: this.normalizeFingerprintMoney(bundle?.bundle_selling_price),
1314
+ price_type: bundle?.price_type ?? '',
1315
+ price_type_ext: bundle?.price_type_ext ?? ''
1316
+ }))),
1317
+ discount_list: this.normalizeFingerprintValue((product?.discount_list || []).map(discount => ({
1318
+ type: discount?.type ?? discount?.tag ?? '',
1319
+ discount_id: discount?.discount_id ?? discount?.order_discount_id ?? discount?.discount?.resource_id ?? null,
1320
+ amount: this.normalizeFingerprintMoney(discount?.amount)
1321
+ })))
1322
+ };
1323
+ }
1324
+ buildOrderProductsFingerprint(products) {
1325
+ const items = (products || []).map(product => this.buildProductFingerprintItem(product));
1326
+ items.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
1327
+ return JSON.stringify(items);
1328
+ }
1329
+ buildProductStructuralFingerprintItem(product) {
1330
+ const sku = product?.product_sku || {};
1331
+ const metadata = product?.metadata || {};
1332
+ const discountTypes = (product?.discount_list || []).map(discount => discount?.type ?? discount?.tag);
1333
+ const extensionType = product?.extension_type || (discountTypes.includes('good_pass') ? 'good_pass' : 'normal');
1334
+ return {
1335
+ product_id: product?.product_id ?? null,
1336
+ product_variant_id: product?.product_variant_id ?? null,
1337
+ extension_type: extensionType,
1338
+ num: (0, _utils3.getSafeProductNum)(product?.num ?? product?.product_quantity),
1339
+ metadata: {
1340
+ unique_identification_number: metadata.unique_identification_number ?? product?.unique_identification_number ?? '',
1341
+ price_override: metadata.price_override ?? '',
1342
+ is_manual_discount: metadata.is_manual_discount ?? '',
1343
+ product_discount_reason: metadata.product_discount_reason ?? ''
1344
+ },
1345
+ product_sku: this.normalizeFingerprintValue({
1346
+ code: sku.code ?? '',
1347
+ barcode: sku.barcode ?? '',
1348
+ variant_id: sku.variant_id ?? product?.product_variant_id ?? 0,
1349
+ variant: sku.variant || [],
1350
+ option: (0, _utils.getProductSkuOptions)(product || {}).map(option => ({
1351
+ id: option?.id ?? option?.option_group_item_id ?? null,
1352
+ option_group_id: option?.option_group_id ?? null,
1353
+ option_group_item_id: option?.option_group_item_id ?? option?.id ?? null,
1354
+ num: Number(option?.num ?? 1) || 1,
1355
+ add_price: this.normalizeFingerprintMoney(option?.add_price ?? option?.price)
1356
+ }))
1357
+ }),
1358
+ product_bundle: this.normalizeFingerprintValue((product?.product_bundle || []).map(bundle => ({
1359
+ bundle_id: bundle?.bundle_id ?? bundle?.id ?? null,
1360
+ bundle_group_id: bundle?.bundle_group_id ?? bundle?.group_id ?? null,
1361
+ bundle_product_id: bundle?.bundle_product_id ?? bundle?._bundle_product_id ?? bundle?.product_id ?? null,
1362
+ bundle_variant_id: bundle?.bundle_variant_id ?? bundle?.product_variant_id ?? 0,
1363
+ num: (0, _utils3.getSafeProductNum)(bundle?.num ?? bundle?.quantity),
1364
+ price_type: bundle?.price_type ?? '',
1365
+ price_type_ext: bundle?.price_type_ext ?? '',
1366
+ option: this.normalizeFingerprintValue((bundle?.option || []).map(option => ({
1367
+ id: option?.id ?? option?.option_group_item_id ?? null,
1368
+ option_group_id: option?.option_group_id ?? null,
1369
+ option_group_item_id: option?.option_group_item_id ?? option?.id ?? null,
1370
+ num: Number(option?.num ?? 1) || 1,
1371
+ add_price: this.normalizeFingerprintMoney(option?.add_price ?? option?.price)
1372
+ })))
1373
+ }))),
1374
+ discount_list: this.normalizeFingerprintValue((product?.discount_list || []).map(discount => ({
1375
+ type: discount?.type ?? discount?.tag ?? '',
1376
+ discount_id: discount?.discount_id ?? discount?.order_discount_id ?? discount?.discount?.resource_id ?? null,
1377
+ amount: this.normalizeFingerprintMoney(discount?.amount)
1378
+ })))
1379
+ };
1380
+ }
1381
+ buildOrderProductsStructuralFingerprint(products) {
1382
+ const items = (products || []).map(product => this.buildProductStructuralFingerprintItem(product));
1383
+ items.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
1384
+ return JSON.stringify(items);
1385
+ }
1386
+ getManualDepositOverride(tempOrder) {
1387
+ const override = tempOrder._extend?.manualDepositOverride;
1388
+ if (!override || typeof override !== 'object') return null;
1389
+ if (typeof override.amount !== 'string') return null;
1390
+ if (typeof override.productStructuralFingerprint !== 'string') return null;
1391
+ return override;
1392
+ }
1393
+ setManualDepositOverride(tempOrder, amount) {
1394
+ tempOrder._extend = {
1395
+ ...(tempOrder._extend || {}),
1396
+ manualDepositOverride: {
1397
+ amount,
1398
+ productStructuralFingerprint: this.buildOrderProductsStructuralFingerprint(tempOrder.products)
1399
+ }
1400
+ };
1401
+ }
1402
+ clearManualDepositOverride(tempOrder) {
1403
+ if (!tempOrder._extend?.manualDepositOverride) return;
1404
+ tempOrder._extend = {
1405
+ ...tempOrder._extend
1406
+ };
1407
+ delete tempOrder._extend.manualDepositOverride;
1408
+ }
1409
+ getOriginalSalesSnapshot(tempOrder) {
1410
+ const snapshot = tempOrder._extend?.originalSalesSnapshot;
1411
+ if (!snapshot || typeof snapshot !== 'object') return null;
1412
+ if (typeof snapshot.productFingerprint !== 'string') return null;
1413
+ return snapshot;
1414
+ }
1415
+ isSnapshotPayableAmountUnchanged(tempOrder, snapshot, snapshotSummary) {
1416
+ const currentPaidPaymentSummary = this.calculatePaidPaymentSummary(tempOrder.payments || []);
1417
+ const currentAmountGap = this.calculateSummaryAmountGap({
1418
+ tempOrder,
1419
+ summary: snapshotSummary,
1420
+ orderPaidAmount: currentPaidPaymentSummary.gapPaidAmount
1421
+ });
1422
+ const snapshotPaidPaymentSummary = this.calculatePaidPaymentSummary(Array.isArray(snapshot.payments) ? snapshot.payments : []);
1423
+ const snapshotAmountGap = this.calculateSummaryAmountGap({
1424
+ tempOrder,
1425
+ summary: snapshotSummary,
1426
+ orderPaidAmount: snapshotPaidPaymentSummary.gapPaidAmount
1427
+ });
1428
+ return currentAmountGap === snapshotAmountGap;
1429
+ }
1430
+ hasShopDiscountChangedFromSnapshot(tempOrder, snapshot) {
1431
+ const snapshotSummary = snapshot.summary && typeof snapshot.summary === 'object' ? snapshot.summary : {};
1432
+ const snapshotShopDiscount = snapshot.shop_discount ?? snapshotSummary.shop_discount;
1433
+ if (snapshotShopDiscount === undefined || snapshotShopDiscount === null || snapshotShopDiscount === '') {
1434
+ return !new _decimal.default(tempOrder.shop_discount || 0).equals(0);
1435
+ }
1436
+ return !new _decimal.default(tempOrder.shop_discount || 0).equals(snapshotShopDiscount || 0);
1437
+ }
1438
+ restoreOriginalSnapshotIfProductsUnchanged(tempOrder, snapshot) {
1439
+ const currentFingerprint = this.buildOrderProductsFingerprint(tempOrder.products);
1440
+ const manualDepositOverride = this.getManualDepositOverride(tempOrder);
1441
+ if (currentFingerprint !== snapshot.productFingerprint) {
1442
+ const currentStructuralFingerprint = this.buildOrderProductsStructuralFingerprint(tempOrder.products);
1443
+ const snapshotStructuralFingerprint = this.buildOrderProductsStructuralFingerprint(Array.isArray(snapshot.products) ? snapshot.products : []);
1444
+ if (currentStructuralFingerprint !== snapshotStructuralFingerprint) return null;
1445
+ }
1446
+ let summary = {
1447
+ ...(0, _utils.createEmptySummary)(),
1448
+ ...(0, _lodashEs.cloneDeep)(snapshot.summary || {})
1449
+ };
1450
+ const shouldUseManualDepositOverride = Boolean(manualDepositOverride && manualDepositOverride.productStructuralFingerprint === this.buildOrderProductsStructuralFingerprint(tempOrder.products));
1451
+ if (shouldUseManualDepositOverride) {
1452
+ summary = this.applyDepositAmountToSummary(tempOrder, summary, manualDepositOverride.amount);
1453
+ } else if (manualDepositOverride) {
1454
+ this.clearManualDepositOverride(tempOrder);
1455
+ }
1456
+ const isPayableAmountUnchanged = this.isSnapshotPayableAmountUnchanged(tempOrder, snapshot, summary);
1457
+ if (!isPayableAmountUnchanged) return null;
1458
+ if (Array.isArray(snapshot.products)) {
1459
+ tempOrder.products = (0, _lodashEs.cloneDeep)(snapshot.products);
1460
+ }
1461
+ if (Array.isArray(snapshot.surcharges)) {
1462
+ tempOrder.surcharges = (0, _lodashEs.cloneDeep)(snapshot.surcharges);
1463
+ }
1464
+ tempOrder.surcharge_fee = summary.surcharge_fee || tempOrder.surcharge_fee;
1465
+ tempOrder.deposit_amount = summary.deposit_amount || '0.00';
1466
+ tempOrder.is_deposit = summary.deposit_amount !== '0.00' ? 1 : 0;
1467
+ const previousSummary = this.store.summary;
1468
+ this.store.summary = summary;
1469
+ this.updateTempOrderPaymentStatus(tempOrder, {
1470
+ previousSummary,
1471
+ summary
1472
+ });
1473
+ return summary;
1474
+ }
1475
+ clearPersistedAmountFieldsForFullRecalc(products) {
1476
+ const moneyKeys = ['surcharge_fee', 'main_product_attached_bundle_surcharge_fee', 'surcharge_rounding_remainder', 'main_product_attached_bundle_tax_fee', 'tax_fee', 'original_tax_fee', 'tax_fee_rounding_remainder', 'original_tax_fee_rounding_remainder'];
1477
+ const metadataMoneyKeys = ['main_product_attached_bundle_surcharge_fee', 'surcharge_rounding_remainder', 'main_product_attached_bundle_tax_fee', 'tax_fee_rounding_remainder'];
1478
+ for (const product of products || []) {
1479
+ const record = product;
1480
+ for (const key of moneyKeys) delete record[key];
1481
+ if (record.metadata && typeof record.metadata === 'object') {
1482
+ record.metadata = {
1483
+ ...record.metadata
1484
+ };
1485
+ for (const key of metadataMoneyKeys) delete record.metadata[key];
1486
+ }
1487
+ for (const bundle of record.product_bundle || []) {
1488
+ for (const key of moneyKeys) delete bundle[key];
1489
+ if (bundle.metadata && typeof bundle.metadata === 'object') {
1490
+ bundle.metadata = {
1491
+ ...bundle.metadata
1492
+ };
1493
+ for (const key of metadataMoneyKeys) delete bundle.metadata[key];
1494
+ }
1495
+ }
1496
+ }
1497
+ }
1498
+ getPaymentTargetAmount() {
1499
+ const tempOrder = this.ensureTempOrder();
1500
+ const summary = this.store.summary || (0, _utils.createEmptySummary)();
1501
+ const paidPaymentSummary = this.calculatePaidPaymentSummary(tempOrder.payments || []);
1502
+ const refundAmount = this.getSummaryRefundAmount(summary);
1503
+ const netPaidAmount = _decimal.default.max(paidPaymentSummary.gapPaidAmount.minus(refundAmount), 0);
1504
+ return this.calculatePaymentTargetAmount(tempOrder, summary, netPaidAmount);
1505
+ }
1506
+ ensureTempOrder() {
1507
+ if (this.store.tempOrder) {
1508
+ this.ensureExternalSaleNumber(this.store.tempOrder);
1509
+ return this.store.tempOrder;
1510
+ }
1511
+ const newOrder = this.createDefaultTempOrderInstance();
1512
+ this.ensureExternalSaleNumber(newOrder);
1513
+ this.store.tempOrder = newOrder;
1514
+ this.persistTempOrder();
1515
+ return newOrder;
1516
+ }
1517
+ restoreOrder() {
1518
+ this.logInfo('restoreOrder start', {});
1519
+ const freshTempOrder = this.createDefaultTempOrderInstance();
1520
+ this.ensureExternalSaleNumber(freshTempOrder);
1521
+ this.store.tempOrder = freshTempOrder;
1522
+ this.store.summary = (0, _utils.createEmptySummary)();
1523
+ this.store.lastOrderInfo = undefined;
1524
+ this.store.syncState = undefined;
1525
+ void this.store.discount?.setOriginalDiscountList([]);
1526
+ void this.store.discount?.setDiscountList([]);
1527
+ this.persistTempOrder();
1528
+ return freshTempOrder;
1529
+ }
1530
+ getTempOrder() {
1531
+ return this.store.tempOrder;
1532
+ }
1533
+ async replaceTempOrder(tempOrder, options) {
1534
+ if (!(0, _utils.isTempOrder)(tempOrder)) {
1535
+ throw new Error('无效的 tempOrder 数据');
1536
+ }
1537
+ const nextTempOrder = this.normalizeTempOrderForRuntime((0, _lodashEs.cloneDeep)(tempOrder), {
1538
+ ensureIdentity: false
1539
+ });
1540
+ this.store.tempOrder = nextTempOrder;
1541
+ if (options?.recalculateSummary !== false) {
1542
+ await this.recalculateSummary({
1543
+ createIfMissing: false
1544
+ });
1545
+ } else {
1546
+ this.store.summary = (0, _utils.createEmptySummary)();
1547
+ }
1548
+ if (options?.persist !== false) {
1549
+ this.persistTempOrder();
1550
+ }
1551
+ if (options?.saveDraft) {
1552
+ await this.saveDraft();
1553
+ }
1554
+ return nextTempOrder;
1555
+ }
1556
+ getOrderIdentity() {
1557
+ const tempOrder = this.store.tempOrder;
1558
+ if (!tempOrder) return null;
1559
+ const lastOrderInfo = this.store.lastOrderInfo || {};
1560
+ const orderId = tempOrder.order_id ?? lastOrderInfo.order_id ?? lastOrderInfo.id ?? null;
1561
+ return {
1562
+ localId: this.cacheId || tempOrder._extend?.localId,
1563
+ orderId,
1564
+ externalSaleNumber: tempOrder.external_sale_number || lastOrderInfo.external_sale_number || '',
1565
+ orderNumber: tempOrder.order_number || lastOrderInfo.order_number || '',
1566
+ shopOrderNumber: tempOrder.shop_order_number || lastOrderInfo.shop_order_number || '',
1567
+ shopFullOrderNumber: tempOrder.shop_full_order_number || lastOrderInfo.shop_full_order_number || '',
1568
+ isSubmitted: orderId !== null && orderId !== undefined
1569
+ };
1570
+ }
1571
+ getOrderSyncState() {
1572
+ const tempOrder = this.store.tempOrder;
1573
+ if (!tempOrder) return 'local';
1574
+ if (this.store.syncState) return this.store.syncState;
1575
+ if (this.store.lastOrderInfo) return 'remoteLoaded';
1576
+ if (tempOrder.order_id) return 'submitted';
1577
+ return 'local';
1578
+ }
1579
+ getOrderAmountSnapshot() {
1580
+ const tempOrder = this.store.tempOrder;
1581
+ if (!tempOrder) return null;
1582
+ const summary = this.store.summary || (0, _utils.createEmptySummary)();
1583
+ const depositAmount = tempOrder.deposit_amount || summary.deposit_amount || '0.00';
1584
+ const paidPaymentSummary = this.calculatePaidPaymentSummary(tempOrder.payments || []);
1585
+ const amountGap = this.calculateSummaryAmountGap({
1586
+ tempOrder,
1587
+ summary,
1588
+ orderPaidAmount: paidPaymentSummary.gapPaidAmount
1589
+ });
1590
+ return {
1591
+ isDeposit: tempOrder.is_deposit === 1,
1592
+ depositAmount,
1593
+ expectAmount: summary.expect_amount || '0.00',
1594
+ totalAmount: summary.total_amount || '0.00',
1595
+ amountGap,
1596
+ summary
1597
+ };
1598
+ }
1599
+ getOrderSnapshot() {
1600
+ const tempOrder = this.store.tempOrder;
1601
+ if (!tempOrder) return null;
1602
+ const identity = this.getOrderIdentity();
1603
+ const amount = this.getOrderAmountSnapshot();
1604
+ if (!identity || !amount) return null;
1605
+ return {
1606
+ identity,
1607
+ syncState: this.getOrderSyncState(),
1608
+ amount,
1609
+ status: tempOrder.status || '',
1610
+ paymentStatus: tempOrder.payment_status || '',
1611
+ shippingStatus: tempOrder.shipping_status || '',
1612
+ note: tempOrder.note || '',
1613
+ customer: {
1614
+ customerId: tempOrder.customer_id ?? null,
1615
+ customerName: tempOrder.customer_name || '',
1616
+ phone: tempOrder.phone || '',
1617
+ email: tempOrder.email || ''
1618
+ },
1619
+ productCount: tempOrder.products?.length || 0,
1620
+ lastOrderInfo: this.store.lastOrderInfo,
1621
+ tempOrder
1622
+ };
1623
+ }
1624
+ async addNewOrder() {
1625
+ const tempOrder = this.ensureTempOrder();
1626
+ this.ensureExternalSaleNumber(tempOrder);
1627
+ await this.recalculateSummary({
1628
+ createIfMissing: true
1629
+ });
1630
+ this.persistTempOrder();
1631
+ if (this.draftPersistEnabled) {
1632
+ await this.saveDraft();
1633
+ }
1634
+ return tempOrder;
1635
+ }
1636
+ getOrderProducts() {
1637
+ const tempOrder = this.ensureTempOrder();
1638
+ return tempOrder.products;
1639
+ }
1640
+ getProductUid(product) {
1641
+ const metadata = product.metadata && typeof product.metadata === 'object' ? product.metadata : {};
1642
+ const uid = metadata.unique_identification_number || product.unique_identification_number || (0, _utils.createUuidV4)();
1643
+ metadata.unique_identification_number = String(uid);
1644
+ product.metadata = metadata;
1645
+ return String(uid);
1646
+ }
1647
+ sanitizeOrderProductForTempOrder(product) {
1648
+ if (product.metadata && typeof product.metadata === 'object') {
1649
+ delete product.metadata.origin;
1650
+ }
1651
+ return product;
1652
+ }
1653
+ sanitizeTempOrderProducts(tempOrder) {
1654
+ tempOrder.products = (tempOrder.products || []).map(product => this.sanitizeOrderProductForTempOrder(product));
1655
+ }
1656
+ ensureExtend(tempOrder) {
1657
+ if (!tempOrder._extend || typeof tempOrder._extend !== 'object') {
1658
+ tempOrder._extend = {
1659
+ productsByUid: {}
1660
+ };
1661
+ }
1662
+ if (!tempOrder._extend.productsByUid) {
1663
+ tempOrder._extend.productsByUid = {};
1664
+ }
1665
+ return tempOrder._extend;
1666
+ }
1667
+ captureProductRuntime(tempOrder, rawProduct, normalizedProduct, existedUid) {
1668
+ const uid = existedUid || this.getProductUid(normalizedProduct);
1669
+ const extend = this.ensureExtend(tempOrder);
1670
+ const existed = extend.productsByUid?.[uid] || {};
1671
+ const origin = rawProduct._origin ?? normalizedProduct._origin ?? existed.origin;
1672
+ extend.productsByUid[uid] = {
1673
+ ...existed,
1674
+ ...(origin !== undefined ? {
1675
+ origin
1676
+ } : {})
1677
+ };
1678
+ delete normalizedProduct._origin;
1679
+ delete normalizedProduct.unique_identification_number;
1680
+ }
1681
+ createLinkedProductAndBooking(params) {
1682
+ const productMetadata = params.product.metadata && typeof params.product.metadata === 'object' ? {
1683
+ ...params.product.metadata
1684
+ } : {};
1685
+ const bookingMetadata = params.booking.metadata && typeof params.booking.metadata === 'object' ? {
1686
+ ...params.booking.metadata
1687
+ } : {};
1688
+ const productUid = String(productMetadata.unique_identification_number || params.product.unique_identification_number || (0, _utils.createUuidV4)());
1689
+ const bookingUid = String(bookingMetadata.unique_identification_number || (0, _utils.createUuidV4)());
1690
+ productMetadata.unique_identification_number = productUid;
1691
+ bookingMetadata.unique_identification_number = bookingUid;
1692
+ this.syncBookingMetadataHolder(params.booking, bookingMetadata);
1693
+ return {
1694
+ productUid,
1695
+ bookingUid,
1696
+ product: {
1697
+ ...params.product,
1698
+ unique_identification_number: productUid,
1699
+ metadata: {
1700
+ ...productMetadata,
1701
+ booking_uid: bookingUid
1702
+ },
1703
+ booking_uid: bookingUid
1704
+ },
1705
+ booking: {
1706
+ ...params.booking,
1707
+ is_all: (0, _utils.normalizeBookingIsAll)(params.booking),
1708
+ product_uid: productUid,
1709
+ sub_type: (0, _utils.normalizeBookingSubType)(params.booking),
1710
+ metadata: bookingMetadata
1711
+ }
1712
+ };
1713
+ }
1714
+
1715
+ /**
1716
+ * booking.holder 是提交真源;metadata.holder 作为详情/回显冗余字段同步保存。
1717
+ *
1718
+ * @example
1719
+ * this.syncBookingMetadataHolder(booking, metadata);
1720
+ */
1721
+ syncBookingMetadataHolder(booking, metadata) {
1722
+ if (booking?.holder !== undefined) {
1723
+ metadata.holder = booking.holder;
1724
+ }
1725
+ }
1726
+
1727
+ /**
1728
+ * 从单条 booking 的 holder / metadata 中收集 holder form_record。
1729
+ *
1730
+ * @example
1731
+ * const ids = this.collectBookingHolderRecords(booking);
1732
+ */
1733
+ collectBookingHolderRecords(booking) {
1734
+ const holder = booking?.holder && typeof booking.holder === 'object' ? booking.holder : null;
1735
+ const source = holder?.form_record ?? booking?.metadata?.holder_id;
1736
+ const records = Array.isArray(source) ? source : [source];
1737
+ return records.filter(record => record !== undefined && record !== null && record !== '');
1738
+ }
1739
+ setProductHolderFromBooking(product, booking) {
1740
+ if (!product || !booking) return;
1741
+ const holderRecords = this.collectBookingHolderRecords(booking);
1742
+ if (!product.metadata || typeof product.metadata !== 'object') {
1743
+ product.metadata = {};
1744
+ }
1745
+ product.metadata.holder_id = holderRecords.length > 0 ? holderRecords : null;
1746
+ }
1747
+ syncLinkedBookingHoldersToProducts(tempOrder) {
1748
+ const bookings = tempOrder.bookings || [];
1749
+ const products = tempOrder.products || [];
1750
+ if (!bookings.length || !products.length) return;
1751
+ const bookingByProductUid = new Map();
1752
+ const bookingByBookingUid = new Map();
1753
+ bookings.forEach(booking => {
1754
+ if (!booking || typeof booking !== 'object') return;
1755
+ const productUid = booking.product_uid || booking.metadata?.product_uid;
1756
+ if (productUid !== undefined && productUid !== null && String(productUid) !== '') {
1757
+ bookingByProductUid.set(String(productUid), booking);
1758
+ }
1759
+ const bookingUid = booking.metadata?.unique_identification_number || booking.booking_uid;
1760
+ if (bookingUid !== undefined && bookingUid !== null && String(bookingUid) !== '') {
1761
+ bookingByBookingUid.set(String(bookingUid), booking);
1762
+ }
1763
+ });
1764
+ products.forEach(product => {
1765
+ if (!product || typeof product !== 'object') return;
1766
+ const productUid = product.metadata?.unique_identification_number || product.unique_identification_number;
1767
+ const bookingUid = product.booking_uid || product.metadata?.booking_uid;
1768
+ const linkedBooking = (productUid !== undefined && productUid !== null && String(productUid) !== '' ? bookingByProductUid.get(String(productUid)) : undefined) || (bookingUid !== undefined && bookingUid !== null && String(bookingUid) !== '' ? bookingByBookingUid.get(String(bookingUid)) : undefined);
1769
+ if (linkedBooking) {
1770
+ this.setProductHolderFromBooking(product, linkedBooking);
1771
+ }
1772
+ });
1773
+ }
1774
+
1775
+ /**
1776
+ * 按 form_record 顺序读取 booking holder 的主字段名称。
1777
+ *
1778
+ * @example
1779
+ * const names = this.collectBookingHolderNames(booking);
1780
+ */
1781
+ collectBookingHolderNames(booking) {
1782
+ const holder = booking?.holder && typeof booking.holder === 'object' ? booking.holder : null;
1783
+ const rawName = holder?.name;
1784
+ if (Array.isArray(rawName)) {
1785
+ return rawName.map(name => String(name || '').trim());
1786
+ }
1787
+ if (typeof rawName === 'string') {
1788
+ return rawName.split(',').map(name => name.trim());
1789
+ }
1790
+ return [];
1791
+ }
1792
+
1793
+ /**
1794
+ * 按老 ticketBooking `formatHolder` 语义汇总订单顶层 holder。
1795
+ *
1796
+ * @example
1797
+ * const holder = this.buildTempOrderHolderFromBookings(tempOrder);
1798
+ */
1799
+ buildTempOrderHolderFromBookings(tempOrder) {
1800
+ const holderRecords = [];
1801
+ const holderNames = [];
1802
+ let baseHolder = null;
1803
+ const seen = new Set();
1804
+ for (const booking of tempOrder.bookings || []) {
1805
+ const holder = booking?.holder && typeof booking.holder === 'object' ? booking.holder : null;
1806
+ if (!baseHolder && holder) {
1807
+ baseHolder = holder;
1808
+ }
1809
+ const records = this.collectBookingHolderRecords(booking);
1810
+ const names = this.collectBookingHolderNames(booking);
1811
+ for (const [index, record] of records.entries()) {
1812
+ const key = String(record);
1813
+ if (seen.has(key)) continue;
1814
+ seen.add(key);
1815
+ holderRecords.push(record);
1816
+ if (names[index]) {
1817
+ holderNames.push(names[index]);
1818
+ }
1819
+ }
1820
+ }
1821
+ if (!holderRecords.length) {
1822
+ return null;
1823
+ }
1824
+ const nextHolder = {
1825
+ customer_id: baseHolder?.customer_id ?? tempOrder.customer_id,
1826
+ form_id: baseHolder?.form_id ?? tempOrder.holder?.form_id ?? 0,
1827
+ form_record: holderRecords
1828
+ };
1829
+ if (holderNames.length) {
1830
+ nextHolder.name = holderNames.join(',');
1831
+ }
1832
+ if (baseHolder?.is_source_shop || this.otherParams?.isFranchisee) {
1833
+ nextHolder.is_source_shop = 1;
1834
+ }
1835
+ return nextHolder;
1836
+ }
1837
+
1838
+ /**
1839
+ * 将 booking 级 holder 汇总到 tempOrder 顶层 holder。
1840
+ *
1841
+ * @example
1842
+ * this.syncTempOrderHolderFromBookings(tempOrder);
1843
+ */
1844
+ syncTempOrderHolderFromBookings(tempOrder) {
1845
+ const bookings = tempOrder.bookings || [];
1846
+ const hasHolderSource = bookings.some(booking => {
1847
+ return booking?.holder !== undefined || booking?.metadata?.holder_id !== undefined;
1848
+ });
1849
+ const nextHolder = this.buildTempOrderHolderFromBookings(tempOrder);
1850
+ if (nextHolder || hasHolderSource || bookings.length === 0) {
1851
+ tempOrder.holder = nextHolder;
1852
+ }
1853
+ this.syncLinkedBookingHoldersToProducts(tempOrder);
1854
+ }
1855
+ getBookingUniqueIdentificationNumber(booking) {
1856
+ const uid = booking?.metadata?.unique_identification_number;
1857
+ if (uid === undefined || uid === null || uid === '') return null;
1858
+ return String(uid);
1859
+ }
1860
+ findBookingIndexByUniqueIdentificationNumber(tempOrder, uniqueIdentificationNumber) {
1861
+ return (tempOrder.bookings || []).findIndex(booking => {
1862
+ return this.getBookingUniqueIdentificationNumber(booking) === uniqueIdentificationNumber;
1863
+ });
1864
+ }
1865
+ removeLinkedBookingsForProduct(tempOrder, product) {
1866
+ const productUid = product?.metadata?.unique_identification_number || product?.unique_identification_number;
1867
+ const bookingUid = product?.booking_uid || product?.metadata?.booking_uid;
1868
+ if (!productUid && !bookingUid) return;
1869
+ tempOrder.bookings = (tempOrder.bookings || []).filter(booking => {
1870
+ const currentBookingUid = this.getBookingUniqueIdentificationNumber(booking);
1871
+ if (bookingUid && currentBookingUid === String(bookingUid)) return false;
1872
+ if (productUid && String(booking?.product_uid || '') === String(productUid)) return false;
1873
+ return true;
1874
+ });
1875
+ }
1876
+
1877
+ // ─── TempOrder: 金额汇总 ───
1878
+
1879
+ getSalesSummary() {
1880
+ if (!this.salesSummaryModuleName) return null;
1881
+ return this.core.getModule(this.salesSummaryModuleName);
1882
+ }
1883
+ applyOrderProductsDepositSummary(tempOrder, summary) {
1884
+ const depositSummary = (0, _utils.calculateOrderProductsDeposit)(tempOrder.products || [], {
1885
+ productsByUid: tempOrder._extend?.productsByUid
1886
+ });
1887
+ const manualDepositOverride = this.getManualDepositOverride(tempOrder);
1888
+ const currentStructuralFingerprint = manualDepositOverride ? this.buildOrderProductsStructuralFingerprint(tempOrder.products) : '';
1889
+ const shouldUseManualDepositOverride = Boolean(manualDepositOverride && manualDepositOverride.productStructuralFingerprint === currentStructuralFingerprint);
1890
+ if (manualDepositOverride && !shouldUseManualDepositOverride) {
1891
+ this.clearManualDepositOverride(tempOrder);
1892
+ }
1893
+ const depositAmount = shouldUseManualDepositOverride ? manualDepositOverride.amount : depositSummary.totalText;
1894
+ const hasDepositAmount = new _decimal.default(depositAmount).gt(0);
1895
+ const nextSummary = {
1896
+ ...summary,
1897
+ deposit_amount: depositAmount,
1898
+ deposit: hasDepositAmount ? {
1899
+ total: depositAmount,
1900
+ deposit_policy_ids: depositSummary.deposit_policy_ids,
1901
+ deposit_policy_data: depositSummary.protocols,
1902
+ hasDeposit: true
1903
+ } : undefined
1904
+ };
1905
+ const isExistingDepositDisabled = tempOrder.is_deposit === 0 && new _decimal.default(tempOrder.deposit_amount || 0).gt(0);
1906
+ tempOrder.deposit_amount = depositAmount;
1907
+ tempOrder.is_deposit = hasDepositAmount && !isExistingDepositDisabled ? 1 : 0;
1908
+ return this.applyLastOrderDepositSnapshot(tempOrder, nextSummary);
1909
+ }
1910
+ async recalculateSummary(options) {
1911
+ const tempOrder = options?.createIfMissing ? this.ensureTempOrder() : this.store.tempOrder;
1912
+ if (!tempOrder) return null;
1913
+ this.sanitizeTempOrderProducts(tempOrder);
1914
+ const originalSnapshot = this.getOriginalSalesSnapshot(tempOrder);
1915
+ const hasShopDiscountChanged = originalSnapshot ? this.hasShopDiscountChangedFromSnapshot(tempOrder, originalSnapshot) : false;
1916
+ const shouldReuseOriginalSnapshot = originalSnapshot && !hasShopDiscountChanged;
1917
+ const snapshotSummary = shouldReuseOriginalSnapshot ? this.restoreOriginalSnapshotIfProductsUnchanged(tempOrder, originalSnapshot) : null;
1918
+ if (snapshotSummary) return snapshotSummary;
1919
+ const shouldFullRecalculateFromSnapshot = Boolean(originalSnapshot);
1920
+ const summaryProducts = shouldFullRecalculateFromSnapshot ? (0, _lodashEs.cloneDeep)(tempOrder.products || []) : tempOrder.products;
1921
+ if (shouldFullRecalculateFromSnapshot) {
1922
+ this.clearPersistedAmountFieldsForFullRecalc(summaryProducts || []);
1923
+ }
1924
+ const salesSummary = this.getSalesSummary();
1925
+ const existedSummary = this.store.summary || (0, _utils.createEmptySummary)();
1926
+ const totalRefundAmount = existedSummary.total_refund_amount || '0.00';
1927
+ const paidPaymentSummary = this.calculatePaidPaymentSummary(tempOrder.payments || []);
1928
+ if (!salesSummary) {
1929
+ const emptyBaseSummary = {
1930
+ ...(0, _utils.createEmptySummary)(),
1931
+ ...existedSummary,
1932
+ total_refund_amount: totalRefundAmount
1933
+ };
1934
+ const emptySummaryBase = {
1935
+ ...emptyBaseSummary,
1936
+ customer_paid_amount: paidPaymentSummary.customerPaidAmount.toFixed(2),
1937
+ order_paid_amount: paidPaymentSummary.orderPaidAmount.toFixed(2),
1938
+ pay_service_charge_amount: paidPaymentSummary.payServiceChargeAmount.toFixed(2),
1939
+ rounding_amount: paidPaymentSummary.roundingAmount.toFixed(2)
1940
+ };
1941
+ const emptySummaryWithDeposit = this.applyOrderProductsDepositSummary(tempOrder, emptySummaryBase);
1942
+ const emptySummary = {
1943
+ ...emptySummaryWithDeposit,
1944
+ amount_gap: this.calculateSummaryAmountGap({
1945
+ tempOrder,
1946
+ summary: emptySummaryWithDeposit,
1947
+ orderPaidAmount: paidPaymentSummary.gapPaidAmount
1948
+ })
1949
+ };
1950
+ tempOrder.surcharge_fee = emptySummary.surcharge_fee;
1951
+ this.store.summary = emptySummary;
1952
+ this.updateTempOrderPaymentStatus(tempOrder, {
1953
+ previousSummary: existedSummary,
1954
+ summary: emptySummary
1955
+ });
1956
+ return this.store.summary;
1957
+ }
1958
+ const salesSummaryResult = await salesSummary.getSummary({
1959
+ products: summaryProducts,
1960
+ isPriceIncludeTax: tempOrder.is_price_include_tax,
1961
+ shopDiscount: tempOrder.shop_discount,
1962
+ ...(this.otherParams?.channel ? {
1963
+ channel: this.otherParams.channel
1964
+ } : {})
1965
+ });
1966
+ if (shouldFullRecalculateFromSnapshot) {
1967
+ tempOrder.products = summaryProducts;
1968
+ }
1969
+ this.syncSummaryProductMetadata(tempOrder.products || []);
1970
+ const roundingAmount = paidPaymentSummary.roundingAmount;
1971
+ const payServiceChargeAmount = paidPaymentSummary.payServiceChargeAmount;
1972
+ const hasSalesSummaryAmount = salesSummaryResult.expect_amount !== undefined || salesSummaryResult.total_amount !== undefined;
1973
+ const expectAmount = hasSalesSummaryAmount ? new _decimal.default(salesSummaryResult.expect_amount ?? salesSummaryResult.total_amount ?? 0) : null;
1974
+ const amountPaymentPatch = hasSalesSummaryAmount || !roundingAmount.eq(0) || !payServiceChargeAmount.eq(0) ? {
1975
+ rounding_amount: roundingAmount.toFixed(2),
1976
+ ...(expectAmount ? {
1977
+ total_amount: _decimal.default.max(0, expectAmount.plus(roundingAmount).plus(payServiceChargeAmount)).toFixed(2)
1978
+ } : {})
1979
+ } : {};
1980
+ const normalizedExpectAmountPatch = expectAmount && salesSummaryResult.expect_amount === undefined ? {
1981
+ expect_amount: expectAmount.toFixed(2)
1982
+ } : {};
1983
+ const summaryForGap = this.applyOrderProductsDepositSummary(tempOrder, {
1984
+ ...salesSummaryResult,
1985
+ total_refund_amount: totalRefundAmount,
1986
+ ...amountPaymentPatch,
1987
+ ...normalizedExpectAmountPatch
1988
+ });
1989
+ const summary = {
1990
+ ...summaryForGap,
1991
+ total_refund_amount: totalRefundAmount,
1992
+ customer_paid_amount: paidPaymentSummary.customerPaidAmount.toFixed(2),
1993
+ order_paid_amount: paidPaymentSummary.orderPaidAmount.toFixed(2),
1994
+ ...amountPaymentPatch,
1995
+ ...normalizedExpectAmountPatch,
1996
+ amount_gap: this.calculateSummaryAmountGap({
1997
+ tempOrder,
1998
+ summary: summaryForGap,
1999
+ orderPaidAmount: paidPaymentSummary.gapPaidAmount
2000
+ }),
2001
+ pay_service_charge_amount: payServiceChargeAmount.toFixed(2)
2002
+ };
2003
+ this.store.summary = summary;
2004
+ if (summary.is_price_include_tax !== undefined) {
2005
+ tempOrder.is_price_include_tax = summary.is_price_include_tax;
2006
+ }
2007
+ if (typeof summary.tax_title === 'string' && summary.tax_title) {
2008
+ tempOrder.tax_title = summary.tax_title;
2009
+ }
2010
+ tempOrder.surcharge_fee = summary.surcharge_fee;
2011
+ tempOrder.surcharges = summary.surcharges || [];
2012
+ this.updateTempOrderPaymentStatus(tempOrder, {
2013
+ previousSummary: existedSummary,
2014
+ summary
2015
+ });
2016
+ return this.store.summary;
2017
+ }
2018
+ async getOrderSummary() {
2019
+ const summary = await this.recalculateSummary({
2020
+ createIfMissing: true
2021
+ });
2022
+ if (!summary) return (0, _utils.createEmptySummary)();
2023
+ this.persistTempOrder();
2024
+ return summary;
2025
+ }
2026
+
2027
+ // ─── TempOrder: 备注 ───
2028
+
2029
+ updateTempOrderNote(note, sync = false) {
2030
+ const tempOrder = this.ensureTempOrder();
2031
+ tempOrder.note = String(note || '');
2032
+ this.persistTempOrder();
2033
+ if (sync) {
2034
+ const orderId = tempOrder.order_id;
2035
+ if (!orderId) return tempOrder.note;
2036
+ return this.syncTempOrderNoteToRemote(orderId, tempOrder.note);
2037
+ }
2038
+ return tempOrder.note;
2039
+ }
2040
+ async syncTempOrderNoteToRemote(orderId, note) {
2041
+ await this.request.put(`/order/order/${orderId}/note`, {
2042
+ note
2043
+ });
2044
+ return note;
2045
+ }
2046
+ getTempOrderNote() {
2047
+ return this.store.tempOrder?.note || '';
2048
+ }
2049
+ updateTempOrderShopDiscount(amount) {
2050
+ const tempOrder = this.ensureTempOrder();
2051
+ const discount = _decimal.default.max(new _decimal.default(Number(amount) || 0), 0).toDecimalPlaces(2).toFixed(2);
2052
+ tempOrder.shop_discount = discount;
2053
+ this.persistTempOrder();
2054
+ return tempOrder.shop_discount;
2055
+ }
2056
+ updateTempOrderDepositAmount(amount) {
2057
+ const tempOrder = this.ensureTempOrder();
2058
+ const depositAmount = this.normalizeDepositAmount(amount);
2059
+ const previousSummary = this.store.summary || (0, _utils.createEmptySummary)();
2060
+ const summaryWithDeposit = this.applyDepositAmountToSummary(tempOrder, {
2061
+ ...(0, _utils.createEmptySummary)(),
2062
+ ...previousSummary
2063
+ }, depositAmount);
2064
+ this.setManualDepositOverride(tempOrder, depositAmount);
2065
+ const paidPaymentSummary = this.calculatePaidPaymentSummary(tempOrder.payments || []);
2066
+ const summary = {
2067
+ ...summaryWithDeposit,
2068
+ amount_gap: this.calculateSummaryAmountGap({
2069
+ tempOrder,
2070
+ summary: summaryWithDeposit,
2071
+ orderPaidAmount: paidPaymentSummary.gapPaidAmount
2072
+ })
2073
+ };
2074
+ this.store.summary = summary;
2075
+ this.updateTempOrderPaymentStatus(tempOrder, {
2076
+ previousSummary,
2077
+ summary
2078
+ });
2079
+ this.persistTempOrder();
2080
+ return summary;
2081
+ }
2082
+ updateTempOrderBuzzer(buzzer) {
2083
+ const tempOrder = this.ensureTempOrder();
2084
+ tempOrder.buzzer = String(buzzer || '');
2085
+ this.persistTempOrder();
2086
+ return tempOrder.buzzer;
2087
+ }
2088
+ updateTempOrderContactsInfo(contactsInfo) {
2089
+ const tempOrder = this.ensureTempOrder();
2090
+ tempOrder.contacts_info = contactsInfo && typeof contactsInfo === 'object' && !Array.isArray(contactsInfo) ? contactsInfo : null;
2091
+ this.persistTempOrder();
2092
+ return tempOrder.contacts_info;
2093
+ }
2094
+ buildTempOrderCustomer(params) {
2095
+ if (!params.customerId) return null;
2096
+ const source = params.source ? (0, _lodashEs.cloneDeep)(params.source) : {};
2097
+ const name = source.name ?? source.display_name ?? source.customerName ?? source.customer_name ?? params.customerName;
2098
+ const customerName = source.customer_name ?? source.customerName ?? source.display_name ?? source.name ?? params.customerName;
2099
+ return {
2100
+ ...source,
2101
+ id: source.id ?? params.customerId,
2102
+ customer_id: source.customer_id ?? params.customerId,
2103
+ name: String(name || ''),
2104
+ customer_name: String(customerName || ''),
2105
+ country_calling_code: String(source.country_calling_code ?? source.countryCallingCode ?? params.countryCallingCode),
2106
+ phone: String(source.phone ?? params.phone),
2107
+ email: String(source.email ?? params.email)
2108
+ };
2109
+ }
2110
+ isCustomerBoundDiscount(discount) {
2111
+ const type = discount?.type ?? discount?.tag;
2112
+ return type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
2113
+ }
2114
+ shouldKeepCustomerBoundDiscountAfterCustomerChange(discount) {
2115
+ return Boolean(discount?.isScan || discount?.isEditMode);
2116
+ }
2117
+ productHasCustomerBoundDiscount(product) {
2118
+ if ((product?.discount_list || []).some(discount => this.isCustomerBoundDiscount(discount))) {
2119
+ return true;
2120
+ }
2121
+ return (product?.product_bundle || []).some(bundle => (bundle?.discount_list || []).some(discount => this.isCustomerBoundDiscount(discount)));
2122
+ }
2123
+ shouldSkipDiscountCalculation(tempOrder) {
2124
+ if (this.hasActiveCustomerBoundDiscount) return false;
2125
+ const discountList = this.store.discount?.getDiscountList?.() || [];
2126
+ if (discountList.length > 0) return false;
2127
+ return !(tempOrder.products || []).some(product => this.productHasCustomerBoundDiscount(product));
2128
+ }
2129
+ clearCustomerBoundDiscounts(tempOrder) {
2130
+ let didRemoveCustomerBoundDiscount = false;
2131
+ const filterDiscountList = discountList => {
2132
+ if (!Array.isArray(discountList)) return [];
2133
+ return discountList.filter(discount => {
2134
+ const shouldRemove = this.isCustomerBoundDiscount(discount);
2135
+ if (shouldRemove) didRemoveCustomerBoundDiscount = true;
2136
+ return !shouldRemove;
2137
+ });
2138
+ };
2139
+ tempOrder.products = (tempOrder.products || []).map(product => {
2140
+ const nextProduct = {
2141
+ ...product,
2142
+ discount_list: filterDiscountList(product.discount_list)
2143
+ };
2144
+ if (Array.isArray(nextProduct.product_bundle)) {
2145
+ nextProduct.product_bundle = nextProduct.product_bundle.map(bundle => ({
2146
+ ...bundle,
2147
+ discount_list: filterDiscountList(bundle.discount_list)
2148
+ }));
2149
+ }
2150
+ return nextProduct;
2151
+ });
2152
+ delete tempOrder.discount_list;
2153
+ if (didRemoveCustomerBoundDiscount) {
2154
+ this.hasActiveCustomerBoundDiscount = true;
2155
+ }
2156
+ const discountModule = this.store.discount;
2157
+ if (!discountModule) return;
2158
+ const currentDiscounts = discountModule.getDiscountList?.() || [];
2159
+ const currentOriginalDiscounts = discountModule.getOriginalDiscountList?.() || [];
2160
+ const nextDiscounts = currentDiscounts.filter(discount => {
2161
+ if (this.shouldKeepCustomerBoundDiscountAfterCustomerChange(discount)) return true;
2162
+ return !this.isCustomerBoundDiscount(discount);
2163
+ });
2164
+ const nextOriginalDiscounts = currentOriginalDiscounts.filter(discount => {
2165
+ if (this.shouldKeepCustomerBoundDiscountAfterCustomerChange(discount)) return true;
2166
+ return !this.isCustomerBoundDiscount(discount);
2167
+ });
2168
+ void discountModule.setDiscountList?.(nextDiscounts);
2169
+ void discountModule.setOriginalDiscountList?.(nextOriginalDiscounts);
2170
+ }
2171
+
2172
+ /**
2173
+ * 更新当前 tempOrder 的客户协议字段。
2174
+ *
2175
+ * PaymentModal 只需要修改订单里的客户事实;如果外部还有客户模块需要同步,
2176
+ * 应由调用方通过回调处理,不能把外部 UI 状态写进 tempOrder。
2177
+ */
2178
+ updateTempOrderCustomer(customer) {
2179
+ const tempOrder = this.ensureTempOrder();
2180
+ const previousCustomerId = tempOrder.customer_id ?? null;
2181
+ const customerId = customer.customer_id ?? customer.customerId ?? customer.id ?? null;
2182
+ const customerName = customer.customer_name ?? customer.customerName ?? customer.display_name ?? customer.name ?? '';
2183
+ tempOrder.customer_id = customerId === null || customerId === undefined || customerId === '' ? null : Number(customerId);
2184
+ tempOrder.customer_name = String(customerName || '');
2185
+ tempOrder.country_calling_code = String(customer.country_calling_code ?? customer.countryCallingCode ?? '');
2186
+ tempOrder.phone = String(customer.phone || '');
2187
+ tempOrder.email = String(customer.email || '');
2188
+ tempOrder.customer = this.buildTempOrderCustomer({
2189
+ source: customer,
2190
+ customerId: tempOrder.customer_id,
2191
+ customerName: tempOrder.customer_name,
2192
+ countryCallingCode: tempOrder.country_calling_code,
2193
+ phone: tempOrder.phone,
2194
+ email: tempOrder.email
2195
+ });
2196
+ const nextCustomerId = tempOrder.customer_id ?? null;
2197
+ this.persistTempOrder();
2198
+ return {
2199
+ customerId: tempOrder.customer_id,
2200
+ customerName: tempOrder.customer_name,
2201
+ phone: tempOrder.phone,
2202
+ email: tempOrder.email
2203
+ };
2204
+ }
2205
+ setOrderCustomer(patch, snapshot) {
2206
+ if (!patch) {
2207
+ this.clearOrderCustomer();
2208
+ return;
2209
+ }
2210
+ const tempOrder = this.ensureTempOrder();
2211
+ const previousCustomerId = tempOrder.customer_id ?? null;
2212
+ tempOrder.customer_id = patch.customer_id || patch.id || null;
2213
+ tempOrder.customer_name = String(patch.customer_name || '');
2214
+ tempOrder.country_calling_code = String(patch.country_calling_code || '');
2215
+ tempOrder.phone = String(patch.phone || '');
2216
+ tempOrder.email = String(patch.email || '');
2217
+ tempOrder.customer = this.buildTempOrderCustomer({
2218
+ source: snapshot || null,
2219
+ customerId: tempOrder.customer_id,
2220
+ customerName: tempOrder.customer_name,
2221
+ countryCallingCode: tempOrder.country_calling_code,
2222
+ phone: tempOrder.phone,
2223
+ email: tempOrder.email
2224
+ });
2225
+ if (snapshot !== undefined) {
2226
+ const extend = this.ensureExtend(tempOrder);
2227
+ if (snapshot === null) delete extend.customerSnapshot;else extend.customerSnapshot = (0, _lodashEs.cloneDeep)(snapshot);
2228
+ }
2229
+ const nextCustomerId = tempOrder.customer_id ?? null;
2230
+ if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '')) {
2231
+ this.store.availableWalletIds = [];
2232
+ (0, _utils.clearTempOrderHolderAssignments)(tempOrder);
2233
+ this.clearCustomerBoundDiscounts(tempOrder);
2234
+ this.applyDiscount();
2235
+ }
2236
+ this.persistTempOrder();
2237
+ this.saveDraftInBackground();
2238
+ if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '')) {
2239
+ this.emitOrderCustomerChange();
2240
+ }
2241
+ }
2242
+ getOrderCustomer() {
2243
+ const tempOrder = this.store.tempOrder;
2244
+ if (!tempOrder || !tempOrder.customer_id) {
2245
+ return null;
2246
+ }
2247
+ return {
2248
+ customer_id: tempOrder.customer_id,
2249
+ customer_name: tempOrder.customer_name || '',
2250
+ country_calling_code: tempOrder.country_calling_code || '',
2251
+ phone: tempOrder.phone || '',
2252
+ email: tempOrder.email || ''
2253
+ };
2254
+ }
2255
+ getOrderCustomerSnapshot() {
2256
+ const snapshot = this.store.tempOrder?._extend?.customerSnapshot;
2257
+ return snapshot ? (0, _lodashEs.cloneDeep)(snapshot) : null;
2258
+ }
2259
+ clearOrderCustomer() {
2260
+ const tempOrder = this.ensureTempOrder();
2261
+ (0, _utils.clearTempOrderHolderAssignments)(tempOrder);
2262
+ tempOrder.customer_id = null;
2263
+ tempOrder.customer_name = '';
2264
+ tempOrder.country_calling_code = '';
2265
+ tempOrder.phone = '';
2266
+ tempOrder.email = '';
2267
+ tempOrder.customer = null;
2268
+ if (tempOrder._extend) delete tempOrder._extend.customerSnapshot;
2269
+ this.store.availableWalletIds = [];
2270
+ this.clearCustomerBoundDiscounts(tempOrder);
2271
+ this.persistTempOrder();
2272
+ this.saveDraftInBackground();
2273
+ this.core.effects.emit(_types.OrderHooks.OnOrderCustomerChange, null);
2274
+ }
2275
+ emitOrderCustomerChange() {
2276
+ const patch = this.getOrderCustomer();
2277
+ if (!patch) {
2278
+ this.core.effects.emit(_types.OrderHooks.OnOrderCustomerChange, null);
2279
+ return;
2280
+ }
2281
+ const payload = {
2282
+ patch,
2283
+ snapshot: this.getOrderCustomerSnapshot()
2284
+ };
2285
+ this.core.effects.emit(_types.OrderHooks.OnOrderCustomerChange, payload);
2286
+ }
2287
+
2288
+ /**
2289
+ * 读取当前订单的 Sales 协议支付项。
2290
+ *
2291
+ * 这里返回的是 tempOrder.payments,而不是旧 PaymentModule 的本地
2292
+ * payment order / tender session。Sales checkout 以该数组为提交事实来源。
2293
+ */
2294
+ getOrderPayments() {
2295
+ const tempOrder = this.ensureTempOrder();
2296
+ return tempOrder.payments || [];
2297
+ }
2298
+
2299
+ /**
2300
+ * 用一组支付来源覆盖当前订单支付项。
2301
+ *
2302
+ * 入参允许来自 PaymentModule 的 PaymentItem,内部统一映射为 Sales 协议:
2303
+ * uuid -> metadata.unique_payment_number,id -> custom_payment_id,
2304
+ * isSynced 等运行态字段不会进入 tempOrder.payments。
2305
+ */
2306
+ setOrderPayments(payments) {
2307
+ const tempOrder = this.ensureTempOrder();
2308
+ tempOrder.payments = (0, _utils.mapPaymentItemsToOrderPayments)(payments);
2309
+ this.persistTempOrder();
2310
+ return tempOrder.payments;
2311
+ }
2312
+
2313
+ /** 追加单个支付项,并立即持久化当前 tempOrder。 */
2314
+ addOrderPayment(payment) {
2315
+ this.logInfo('addOrderPayment', {
2316
+ payment
2317
+ });
2318
+ const tempOrder = this.ensureTempOrder();
2319
+ const orderPaymentType = this.resolveNextOrderPaymentType(tempOrder);
2320
+ const mapped = (0, _utils.mapPaymentItemsToOrderPayments)([this.normalizePaymentSource({
2321
+ ...payment,
2322
+ type: orderPaymentType,
2323
+ order_payment_type: orderPaymentType
2324
+ }, {
2325
+ defaultPaid: true
2326
+ })]);
2327
+ tempOrder.payments = [...(tempOrder.payments || []), ...mapped];
2328
+ this.persistTempOrder();
2329
+ void this.recalculateSummary({
2330
+ createIfMissing: true
2331
+ }).catch(error => {
2332
+ this.logError('recalculate summary after addOrderPayment failed', {
2333
+ error: error instanceof Error ? error.message : String(error)
2334
+ });
2335
+ });
2336
+ return tempOrder.payments;
2337
+ }
2338
+
2339
+ /**
2340
+ * 按支付唯一号、后端支付 ID、自定义支付方式 ID 或旧 uuid 更新支付项。
2341
+ *
2342
+ * 该方法服务 PaymentModal 的 void / 编辑场景;更新 metadata 时会做浅合并,
2343
+ * 避免覆盖 unique_payment_number、现金实付/找零等已有元数据。
2344
+ */
2345
+ updateOrderPayment(paymentIdentity, updates) {
2346
+ const tempOrder = this.ensureTempOrder();
2347
+ const identity = String(paymentIdentity);
2348
+ tempOrder.payments = (tempOrder.payments || []).map(payment => {
2349
+ const uniquePaymentNumber = payment?.metadata?.unique_payment_number;
2350
+ const orderPaymentId = payment?.order_payment_id;
2351
+ const customPaymentId = payment?.custom_payment_id;
2352
+ const uuid = payment?.uuid;
2353
+ const matched = [uniquePaymentNumber, orderPaymentId, customPaymentId, uuid].some(value => value !== undefined && String(value) === identity);
2354
+ if (!matched) return payment;
2355
+ return {
2356
+ ...payment,
2357
+ ...updates,
2358
+ metadata: {
2359
+ ...(payment.metadata || {}),
2360
+ ...(updates.metadata || {})
2361
+ }
2362
+ };
2363
+ });
2364
+ this.persistTempOrder();
2365
+ return tempOrder.payments;
2366
+ }
2367
+
2368
+ /** 从当前订单支付项中移除指定支付项。 */
2369
+ deleteOrderPayment(paymentIdentity) {
2370
+ const tempOrder = this.ensureTempOrder();
2371
+ const identity = String(paymentIdentity);
2372
+ tempOrder.payments = (tempOrder.payments || []).filter(payment => {
2373
+ const uniquePaymentNumber = payment?.metadata?.unique_payment_number;
2374
+ const orderPaymentId = payment?.order_payment_id;
2375
+ const customPaymentId = payment?.custom_payment_id;
2376
+ const uuid = payment?.uuid;
2377
+ return ![uniquePaymentNumber, orderPaymentId, customPaymentId, uuid].some(value => value !== undefined && String(value) === identity);
2378
+ });
2379
+ this.persistTempOrder();
2380
+ return tempOrder.payments;
2381
+ }
2382
+ confirmPendingVoucherPaymentsInList(payments) {
2383
+ return (payments || []).map(payment => {
2384
+ if (!isPendingVoucherPaymentRecord(payment)) return payment;
2385
+ return {
2386
+ ...payment,
2387
+ status: ORDER_PAYMENT_STATUS_PAID
2388
+ };
2389
+ });
2390
+ }
2391
+ discardPendingVoucherPaymentsFromList(payments) {
2392
+ return (payments || []).filter(payment => !isPendingVoucherPaymentRecord(payment));
2393
+ }
2394
+ prepareVoucherPaymentsForSubmit(payments, confirmPendingVoucherPayments = true) {
2395
+ const mappedPayments = (0, _utils.mapPaymentItemsToOrderPayments)(payments || []);
2396
+ return confirmPendingVoucherPayments ? this.confirmPendingVoucherPaymentsInList(mappedPayments) : this.discardPendingVoucherPaymentsFromList(mappedPayments);
2397
+ }
2398
+ applyPaymentLifecycleChange(tempOrder, options) {
2399
+ this.updateTempOrderPaymentStatus(tempOrder);
2400
+ if (options?.persist !== false) {
2401
+ this.persistTempOrder();
2402
+ }
2403
+ if (options?.recalculateSummary === false) return;
2404
+ const logContext = options?.logContext || 'payment lifecycle change';
2405
+ void this.recalculateSummary({
2406
+ createIfMissing: true
2407
+ }).catch(error => {
2408
+ this.logError(`recalculate summary after ${logContext} failed`, {
2409
+ error: error instanceof Error ? error.message : String(error)
2410
+ });
2411
+ });
2412
+ }
2413
+ confirmPendingVoucherPayments(options) {
2414
+ const tempOrder = this.ensureTempOrder();
2415
+ const payments = tempOrder.payments || [];
2416
+ const hasPendingVoucher = payments.some(payment => isPendingVoucherPaymentRecord(payment));
2417
+ if (!hasPendingVoucher) return payments;
2418
+ tempOrder.payments = this.confirmPendingVoucherPaymentsInList(payments);
2419
+ this.applyPaymentLifecycleChange(tempOrder, {
2420
+ ...options,
2421
+ logContext: 'confirmPendingVoucherPayments'
2422
+ });
2423
+ // this.recalculateSummary();
2424
+ return tempOrder.payments;
2425
+ }
2426
+ discardPendingVoucherPayments(options) {
2427
+ const tempOrder = this.ensureTempOrder();
2428
+ const payments = tempOrder.payments || [];
2429
+ const nextPayments = this.discardPendingVoucherPaymentsFromList(payments);
2430
+ if (nextPayments.length === payments.length) return payments;
2431
+ tempOrder.payments = nextPayments;
2432
+ this.applyPaymentLifecycleChange(tempOrder, {
2433
+ ...options,
2434
+ logContext: 'discardPendingVoucherPayments'
2435
+ });
2436
+ return tempOrder.payments;
2437
+ }
2438
+
2439
+ /**
2440
+ * 覆盖当前订单中的 wallet pass 支付项。
2441
+ *
2442
+ * 判断口径与 PaymentModal 保持一致:voucher_id 存在且不为 0 的支付项
2443
+ * 视为 wallet pass;其它支付项保持不变。
2444
+ */
2445
+ updateVoucherOrderPayments(payments, options) {
2446
+ const tempOrder = this.ensureTempOrder();
2447
+ const isOrderPaymentType = value => {
2448
+ return value === 'normal' || value === 'deposit';
2449
+ };
2450
+ const voucherOrderPaymentType = this.resolveNextOrderPaymentType(tempOrder);
2451
+ const withVoucherPaymentType = payment => {
2452
+ const paymentRecord = payment;
2453
+ if (!isVoucherPaymentRecord(paymentRecord)) return payment;
2454
+ const explicitPaymentType = isOrderPaymentType(paymentRecord.order_payment_type) ? paymentRecord.order_payment_type : isOrderPaymentType(paymentRecord.type) ? paymentRecord.type : undefined;
2455
+ const nextPaymentType = explicitPaymentType || voucherOrderPaymentType;
2456
+ const shouldPreserveCustomPaymentType = paymentRecord.custom_payment_type === undefined && paymentRecord.type !== undefined && !isOrderPaymentType(paymentRecord.type);
2457
+ return {
2458
+ ...paymentRecord,
2459
+ ...(shouldPreserveCustomPaymentType ? {
2460
+ custom_payment_type: paymentRecord.type
2461
+ } : {}),
2462
+ type: nextPaymentType,
2463
+ order_payment_type: nextPaymentType
2464
+ };
2465
+ };
2466
+ const normalizeVoucherPaymentStatus = payment => {
2467
+ const paymentRecord = payment;
2468
+ if (!isVoucherPaymentRecord(paymentRecord)) return payment;
2469
+ if (hasSyncedOrderPaymentRecord(paymentRecord) && (paymentRecord.status === undefined || paymentRecord.status === 'active')) {
2470
+ return {
2471
+ ...paymentRecord,
2472
+ status: ORDER_PAYMENT_STATUS_PAID
2473
+ };
2474
+ }
2475
+ if (paymentRecord.status !== undefined && paymentRecord.status !== 'active') {
2476
+ return payment;
2477
+ }
2478
+ return {
2479
+ ...paymentRecord,
2480
+ status: options?.status || ORDER_PAYMENT_STATUS_PENDING
2481
+ };
2482
+ };
2483
+ const mappedVouchers = (0, _utils.mapPaymentItemsToOrderPayments)(payments.map(payment => this.normalizePaymentSource(normalizeVoucherPaymentStatus(withVoucherPaymentType(payment)), {
2484
+ defaultPaid: false
2485
+ }))).filter(payment => {
2486
+ return isVoucherPaymentRecord(payment);
2487
+ });
2488
+ const nonVoucherPayments = (tempOrder.payments || []).filter(payment => {
2489
+ return !isVoucherPaymentRecord(payment);
2490
+ });
2491
+ const normalizeCommittedVoucherPayment = payment => {
2492
+ if (hasSyncedOrderPaymentRecord(payment) && (payment.status === undefined || payment.status === 'active')) {
2493
+ return {
2494
+ ...payment,
2495
+ status: ORDER_PAYMENT_STATUS_PAID
2496
+ };
2497
+ }
2498
+ return payment;
2499
+ };
2500
+ const committedVoucherPayments = (tempOrder.payments || []).filter(payment => isCommittedVoucherPaymentRecord(payment)).map(payment => normalizeCommittedVoucherPayment(payment));
2501
+ const committedVoucherPaymentIds = new Set(committedVoucherPayments.filter(payment => hasSyncedOrderPaymentRecord(payment)).map(payment => String(payment.order_payment_id)));
2502
+ const committedVoucherIds = new Set(committedVoucherPayments.map(payment => String(payment.voucher_id)));
2503
+ const replaceableVouchers = mappedVouchers.filter(payment => {
2504
+ if (hasSyncedOrderPaymentRecord(payment) && committedVoucherPaymentIds.has(String(payment.order_payment_id))) {
2505
+ return false;
2506
+ }
2507
+ if (payment.voucher_id !== undefined && committedVoucherIds.has(String(payment.voucher_id))) {
2508
+ return false;
2509
+ }
2510
+ return true;
2511
+ });
2512
+ const cappedVouchers = this.capVoucherPaymentsByRemainingAmount({
2513
+ vouchers: replaceableVouchers,
2514
+ nonVoucherPayments: [...nonVoucherPayments, ...committedVoucherPayments]
2515
+ });
2516
+ tempOrder.payments = [...nonVoucherPayments, ...committedVoucherPayments, ...cappedVouchers];
2517
+ this.updateTempOrderPaymentStatus(tempOrder);
2518
+ this.persistTempOrder();
2519
+ void this.recalculateSummary({
2520
+ createIfMissing: true
2521
+ }).catch(error => {
2522
+ this.logError('recalculate summary after updateVoucherOrderPayments failed', {
2523
+ error: error instanceof Error ? error.message : String(error)
2524
+ });
2525
+ });
2526
+ return tempOrder.payments;
2527
+ }
2528
+ async shouldMergeProductToOrder(params) {
2529
+ const onBeforeMergeProductToOrder = this.orderHooks?.onBeforeMergeProductToOrder;
2530
+ if (!onBeforeMergeProductToOrder) return true;
2531
+ const result = await onBeforeMergeProductToOrder({
2532
+ ...params,
2533
+ defaultShouldMerge: true
2534
+ });
2535
+ if (typeof result === 'boolean') return result;
2536
+ if (result && typeof result === 'object' && typeof result.shouldMerge === 'boolean') {
2537
+ return result.shouldMerge;
2538
+ }
2539
+ return true;
2540
+ }
2541
+ hasOrderProductLineNote(product) {
2542
+ return typeof product?.note === 'string' && product.note.trim().length > 0;
2543
+ }
2544
+
2545
+ /**
2546
+ * 判断商品行是否显式绑定了 booking。
2547
+ *
2548
+ * @example
2549
+ * this.hasLinkedBookingUid({ metadata: { booking_uid: 'booking-1' } });
2550
+ */
2551
+ hasLinkedBookingUid(product) {
2552
+ const bookingUid = product?.booking_uid ?? product?.metadata?.booking_uid;
2553
+ return bookingUid !== undefined && bookingUid !== null && String(bookingUid) !== '';
2554
+ }
2555
+
2556
+ // ─── TempOrder: 商品 CRUD ───
2557
+
2558
+ /**
2559
+ * 预约商品加车后的统一收尾:促销、折扣、summary 重算与持久化(批量拆行时只调用一次)。
2560
+ *
2561
+ * @example
2562
+ * await this.finalizeProductOrderMutation(tempOrder);
2563
+ */
2564
+ async finalizeProductOrderMutation(tempOrder, options) {
2565
+ this.syncTempOrderHolderFromBookings(tempOrder);
2566
+ if (!options?.skipEditDiscountConfigRefresh) {
2567
+ await this.ensureEditDiscountConfigForProductChange(tempOrder);
2568
+ }
2569
+ if (!options?.skipDiscountRecalculation) {
2570
+ await this.applyPromotion();
2571
+ }
2572
+ if (!options?.skipDiscountRecalculation) {
2573
+ this.applyDiscount();
2574
+ }
2575
+ this.sanitizeTempOrderProducts(tempOrder);
2576
+ await this.recalculateSummary({
2577
+ createIfMissing: true
2578
+ });
2579
+ this.persistTempOrder();
2580
+ }
2581
+
2582
+ /**
2583
+ * 向 tempOrder 追加一条商品行(及可选 booking),不触发 summary 重算。
2584
+ * 带 booking 且 num>1 时由 `addProductToOrder` 拆成多次本方法调用(每次 num=1)。
2585
+ *
2586
+ * @example
2587
+ * await this.appendProductLineToTempOrder(tempOrder, { product_id: 1, num: 1 }, booking);
2588
+ */
2589
+ async appendProductLineToTempOrder(tempOrder, product, booking) {
2590
+ const linked = booking ? this.createLinkedProductAndBooking({
2591
+ product,
2592
+ booking
2593
+ }) : null;
2594
+ const productToAdd = linked?.product || product;
2595
+ const incomingFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(productToAdd), productToAdd.product_bundle);
2596
+ const normalizedIncoming = (0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd));
2597
+ const incomingBookingUid = productToAdd.booking_uid ?? productToAdd.metadata?.booking_uid;
2598
+ if (incomingBookingUid !== undefined && incomingBookingUid !== null && String(incomingBookingUid) !== '') {
2599
+ normalizedIncoming.booking_uid = String(incomingBookingUid);
2600
+ normalizedIncoming.metadata = {
2601
+ ...(normalizedIncoming.metadata || {}),
2602
+ booking_uid: String(incomingBookingUid)
2603
+ };
2604
+ }
2605
+ normalizedIncoming.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedIncoming.discount_list);
2606
+ const hasIncomingGoodPass = this.hasGoodPassDiscount(normalizedIncoming);
2607
+ const hasIncomingProductNote = this.hasOrderProductLineNote(normalizedIncoming);
2608
+ const hasIncomingBookingUid = this.hasLinkedBookingUid(productToAdd) || this.hasLinkedBookingUid(normalizedIncoming);
2609
+ const shouldForceNewLine = !!booking || hasIncomingProductNote || hasIncomingBookingUid;
2610
+ const matchedIndex = hasIncomingGoodPass || shouldForceNewLine ? -1 : tempOrder.products.findIndex(item => {
2611
+ if (this.hasGoodPassDiscount(item)) return false;
2612
+ if (this.hasOrderProductLineNote(item)) return false;
2613
+ if (item.order_detail_id !== undefined && item.order_detail_id !== null) return false;
2614
+ if (item.metadata?.is_edit_for_runtime) return false;
2615
+ if (item.product_id !== productToAdd.product_id) return false;
2616
+ if (item.product_variant_id !== productToAdd.product_variant_id) return false;
2617
+ const existedFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(item), item.product_bundle);
2618
+ return existedFingerprint === incomingFingerprint;
2619
+ });
2620
+ const matchedProduct = matchedIndex === -1 ? null : tempOrder.products[matchedIndex];
2621
+ const existedFingerprint = matchedProduct ? (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(matchedProduct), matchedProduct.product_bundle) : '';
2622
+ const shouldMerge = matchedProduct ? await this.shouldMergeProductToOrder({
2623
+ incomingProduct: productToAdd,
2624
+ normalizedIncoming,
2625
+ matchedProduct,
2626
+ matchedIndex,
2627
+ incomingFingerprint,
2628
+ existedFingerprint,
2629
+ tempOrder,
2630
+ booking
2631
+ }) : false;
2632
+ if (matchedIndex === -1 || !matchedProduct || !shouldMerge) {
2633
+ this.captureProductRuntime(tempOrder, productToAdd, normalizedIncoming);
2634
+ if (linked) {
2635
+ normalizedIncoming.booking_uid = linked.bookingUid;
2636
+ normalizedIncoming.metadata = {
2637
+ ...(normalizedIncoming.metadata || {}),
2638
+ booking_uid: linked.bookingUid
2639
+ };
2640
+ }
2641
+ // tempOrder.products.push(normalizedIncoming);
2642
+ tempOrder.products = [...tempOrder.products, normalizedIncoming];
2643
+ } else {
2644
+ const targetProduct = matchedProduct;
2645
+ const targetUid = targetProduct.metadata?.unique_identification_number;
2646
+ const normalizedProduct = (0, _utils.mergeOrderProductDisplayFields)(targetProduct, (0, _utils3.normalizeOrderProduct)({
2647
+ ...productToAdd,
2648
+ metadata: {
2649
+ ...(productToAdd.metadata || {}),
2650
+ unique_identification_number: targetUid
2651
+ }
2652
+ }));
2653
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
2654
+ const nextNum = (0, _utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num);
2655
+ const targetIsManualProductDiscount = isManualProductDiscountProduct(targetProduct);
2656
+ const nextDiscountList = targetIsManualProductDiscount ? (0, _utils.normalizeOrderProductDiscountList)((0, _manualProductDiscount.syncManualProductDiscountProductNum)(targetProduct.discount_list, nextNum)) : normalizedProduct.discount_list;
2657
+ tempOrder.products[matchedIndex] = {
2658
+ ...targetProduct,
2659
+ ...normalizedProduct,
2660
+ discount_list: nextDiscountList,
2661
+ metadata: {
2662
+ ...(targetProduct.metadata || {}),
2663
+ ...(normalizedProduct.metadata || {}),
2664
+ unique_identification_number: targetUid
2665
+ },
2666
+ product_sku: {
2667
+ ...(targetProduct.product_sku || {
2668
+ option: []
2669
+ }),
2670
+ ...(normalizedProduct.product_sku || {
2671
+ option: []
2672
+ }),
2673
+ option: (0, _utils.getProductSkuOptions)(normalizedProduct)
2674
+ },
2675
+ ...(targetIsManualProductDiscount ? {
2676
+ selling_price: targetProduct.selling_price,
2677
+ original_price: targetProduct.original_price,
2678
+ payment_price: targetProduct.payment_price,
2679
+ metadata: {
2680
+ ...(normalizedProduct.metadata || {}),
2681
+ ...(targetProduct.metadata || {}),
2682
+ unique_identification_number: targetUid
2683
+ }
2684
+ } : {}),
2685
+ note: targetProduct.note,
2686
+ order_detail_id: targetProduct.order_detail_id,
2687
+ num: nextNum
2688
+ };
2689
+ this.captureProductRuntime(tempOrder, productToAdd, tempOrder.products[matchedIndex], targetUid);
2690
+ }
2691
+ if (linked) {
2692
+ tempOrder.bookings = [...(tempOrder.bookings || []), linked.booking];
2693
+ }
2694
+ }
2695
+
2696
+ /**
2697
+ * 添加商品行。带 booking 时强制独立行;num>1 时拆成多条 num=1 的 product+booking(对齐 BookingByStep / ticketBooking addService)。
2698
+ *
2699
+ * @example
2700
+ * await order.addProductToOrder({ product_id: 1, num: 3 }, { start_time: '10:00' });
2701
+ * // → tempOrder.products 新增 3 行,每行 num=1,bookings 3 条
2702
+ */
2703
+ async addProductToOrder(product, booking) {
2704
+ const tempOrder = this.ensureTempOrder();
2705
+ if (booking) {
2706
+ const productRecord = product;
2707
+ const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
2708
+ if (splitCount > 1) {
2709
+ for (let i = 0; i < splitCount; i += 1) {
2710
+ const singleLine = (0, _lodashEs.cloneDeep)(productRecord);
2711
+ singleLine.num = 1;
2712
+ delete singleLine.product_quantity;
2713
+ await this.appendProductLineToTempOrder(tempOrder, singleLine, (0, _lodashEs.cloneDeep)(booking));
2714
+ }
2715
+ await this.finalizeProductOrderMutation(tempOrder);
2716
+ return tempOrder.products;
2717
+ }
2718
+ }
2719
+ await this.appendProductLineToTempOrder(tempOrder, product, booking);
2720
+ await this.finalizeProductOrderMutation(tempOrder);
2721
+ this.logInfo('addProductToOrder success', {
2722
+ product_id: product.product_id,
2723
+ with_booking: !!booking
2724
+ });
2725
+ return tempOrder.products;
2726
+ }
2727
+
2728
+ /**
2729
+ * 批量添加商品行,所有行追加完成后只触发一次促销、折扣、summary 重算与持久化。
2730
+ *
2731
+ * @example
2732
+ * await order.addProductsToOrder([
2733
+ * { product: { product_id: 1, product_variant_id: 0, num: 1 } },
2734
+ * { product: { product_id: 2, product_variant_id: 0, num: 1 } },
2735
+ * ]);
2736
+ */
2737
+ async addProductsToOrder(items, options) {
2738
+ const tempOrder = this.ensureTempOrder();
2739
+ if (!Array.isArray(items) || items.length === 0) return tempOrder.products;
2740
+ for (const item of items || []) {
2741
+ const {
2742
+ product,
2743
+ booking
2744
+ } = item;
2745
+ if (!product) continue;
2746
+ if (booking) {
2747
+ const productRecord = product;
2748
+ const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
2749
+ if (splitCount > 1) {
2750
+ for (let i = 0; i < splitCount; i += 1) {
2751
+ const singleLine = (0, _lodashEs.cloneDeep)(productRecord);
2752
+ singleLine.num = 1;
2753
+ delete singleLine.product_quantity;
2754
+ await this.appendProductLineToTempOrder(tempOrder, singleLine, (0, _lodashEs.cloneDeep)(booking));
2755
+ }
2756
+ continue;
2757
+ }
2758
+ }
2759
+ await this.appendProductLineToTempOrder(tempOrder, product, booking);
2760
+ }
2761
+ await this.finalizeProductOrderMutation(tempOrder, options);
2762
+ this.logInfo('addProductsToOrder success', {
2763
+ itemsCount: items.length
2764
+ });
2765
+ return tempOrder.products;
2766
+ }
2767
+ hasGoodPassDiscount(product) {
2768
+ const hasMainGoodPass = (product.discount_list || []).some(item => item?.type === 'good_pass' || item?.tag === 'good_pass');
2769
+ if (hasMainGoodPass) return true;
2770
+ const bundleDiscountList = [];
2771
+ (product.product_bundle || []).forEach(item => {
2772
+ bundleDiscountList.push(...(item?.discount_list || []));
2773
+ });
2774
+ (product?._extend?.other?.bundle || []).forEach(item => {
2775
+ bundleDiscountList.push(...(item?.discount_list || []));
2776
+ });
2777
+ return bundleDiscountList.some(item => item?.type === 'good_pass' || item?.tag === 'good_pass');
2778
+ }
2779
+ getBundleRuntimeIdentity(bundle) {
2780
+ if (!bundle || typeof bundle !== 'object') return '';
2781
+ return [bundle.bundle_id ?? bundle.id ?? '', bundle.bundle_group_id ?? bundle.group_id ?? '', bundle.bundle_product_id ?? bundle._bundle_product_id ?? bundle.product_id ?? '', bundle.bundle_variant_id ?? bundle.product_variant_id ?? 0].join('|');
2782
+ }
2783
+ preservePersistedBundlePriceFields(previousBundles, nextBundles) {
2784
+ if (!Array.isArray(nextBundles)) return nextBundles;
2785
+ if (!Array.isArray(previousBundles) || previousBundles.length === 0) return nextBundles;
2786
+ const previousByIdentity = new Map();
2787
+ previousBundles.forEach(bundle => {
2788
+ const identity = this.getBundleRuntimeIdentity(bundle);
2789
+ if (identity) previousByIdentity.set(identity, bundle);
2790
+ });
2791
+ return nextBundles.map((bundle, index) => {
2792
+ if (!bundle || typeof bundle !== 'object') return bundle;
2793
+ const previous = previousByIdentity.get(this.getBundleRuntimeIdentity(bundle)) || previousBundles[index];
2794
+ if (!previous || typeof previous !== 'object') return bundle;
2795
+ return {
2796
+ ...bundle,
2797
+ ...((bundle.cover === undefined || bundle.cover === null || bundle.cover === '') && previous.cover !== undefined && previous.cover !== null && previous.cover !== '' ? {
2798
+ cover: previous.cover
2799
+ } : {}),
2800
+ price: previous.price,
2801
+ custom_price: previous.custom_price,
2802
+ bundle_payment_price: previous.bundle_payment_price,
2803
+ bundle_selling_price: previous.bundle_selling_price,
2804
+ original_price: previous.original_price,
2805
+ product_price: previous.product_price
2806
+ };
2807
+ });
2808
+ }
2809
+ applyOrderProductUpdateToTempOrder(tempOrder, params) {
2810
+ const {
2811
+ product_id,
2812
+ product_variant_id,
2813
+ updates,
2814
+ unique_identification_number,
2815
+ identity_key,
2816
+ product_sku,
2817
+ product_bundle,
2818
+ booking
2819
+ } = params;
2820
+ const identityLookup = {
2821
+ product_id,
2822
+ product_variant_id
2823
+ };
2824
+ if (unique_identification_number !== undefined) {
2825
+ identityLookup.unique_identification_number = unique_identification_number;
2826
+ if (identity_key === undefined) {
2827
+ identityLookup.identity_key = unique_identification_number;
2828
+ }
2829
+ }
2830
+ if (identity_key !== undefined) {
2831
+ identityLookup.identity_key = identity_key;
2832
+ }
2833
+ if (product_sku !== undefined) identityLookup.product_sku = product_sku;
2834
+ if (product_bundle !== undefined) identityLookup.product_bundle = product_bundle;
2835
+ let productIndex = -1;
2836
+ if (unique_identification_number !== undefined) {
2837
+ const targetUid = String(unique_identification_number);
2838
+ productIndex = tempOrder.products.findIndex(item => {
2839
+ return String(item.metadata?.unique_identification_number || item.unique_identification_number || '') === targetUid;
2840
+ });
2841
+ }
2842
+ if (productIndex === -1) {
2843
+ productIndex = (0, _utils3.getProductIdentityIndex)(tempOrder.products, identityLookup);
2844
+ }
2845
+ if (productIndex === -1) {
2846
+ throw new Error('[Order] 目标商品不存在,无法更新');
2847
+ }
2848
+ const targetProduct = tempOrder.products[productIndex];
2849
+ const nextProduct = {
2850
+ ...targetProduct,
2851
+ ...updates,
2852
+ product_id,
2853
+ product_variant_id,
2854
+ metadata: {
2855
+ ...(targetProduct.metadata || {}),
2856
+ ...(updates?.metadata || {}),
2857
+ unique_identification_number: targetProduct.metadata?.unique_identification_number || unique_identification_number
2858
+ },
2859
+ product_sku: {
2860
+ ...(targetProduct.product_sku || {
2861
+ option: []
2862
+ }),
2863
+ ...(updates?.product_sku && typeof updates.product_sku === 'object' ? updates.product_sku : {}),
2864
+ option: Array.isArray(updates?.product_sku?.option) ? updates.product_sku.option : (0, _utils.getProductSkuOptions)(targetProduct)
2865
+ }
2866
+ };
2867
+ nextProduct.num = (0, _utils3.getSafeProductNum)(nextProduct.num);
2868
+ // 兜底:caller 通过 updates 直接改了 selling_price / original_price 时,
2869
+ // 视为 "重新给 source-level 主价"(caller 侧旧约定仍是 main-only 单价)。
2870
+ // 清掉 metadata 里的 source_product_price / main_product_* / price_schema_version,
2871
+ // 让 normalize 把顶层 selling_price 当作 source 来重算 main_original(=source+options) /
2872
+ // main_selling / composite;否则旧 metadata 会反压顶层新值,触发口径不一致。
2873
+ // 如果 caller 走推荐路径(updates.metadata.* 显式指定主价),保留 metadata 由 normalize 认它。
2874
+ const callerUpdatesTopPrice = Object.prototype.hasOwnProperty.call(updates || {}, 'selling_price') || Object.prototype.hasOwnProperty.call(updates || {}, 'original_price');
2875
+ const callerUpdatesMainMeta = updates?.metadata?.main_product_selling_price !== undefined || updates?.metadata?.main_product_original_price !== undefined || updates?.metadata?.source_product_price !== undefined;
2876
+ const callerUpdatesSourcePrice = updates?.metadata?.source_product_price !== undefined;
2877
+ const callerUpdatesMainSellingPrice = updates?.metadata?.main_product_selling_price !== undefined;
2878
+ const callerUpdatesMainOriginalPrice = updates?.metadata?.main_product_original_price !== undefined;
2879
+ const previousLineFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(targetProduct), targetProduct.product_bundle);
2880
+ const nextLineFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(nextProduct), nextProduct.product_bundle);
2881
+ const callerChangesVariant = Number(product_variant_id || 0) !== Number(targetProduct.product_variant_id || 0);
2882
+ const callerChangesLineConfiguration = callerChangesVariant || previousLineFingerprint !== nextLineFingerprint;
2883
+ const isPersistedOrderLine = targetProduct.order_detail_id !== undefined && targetProduct.order_detail_id !== null;
2884
+ const callerChangesDiscountList = Array.isArray(updates?.discount_list) && updates.discount_list.length > 0 && !isSameDiscountList(updates.discount_list, targetProduct.discount_list);
2885
+ const callerUpdatesManualPrice = updates?.metadata?.price_override !== undefined || updates?.metadata?.is_manual_discount !== undefined || updates?.metadata?.product_discount_reason !== undefined || callerChangesDiscountList;
2886
+ const callerAllowsBookingReprice = (() => {
2887
+ if (params.allow_booking_reprice !== true || !booking || !callerUpdatesTopPrice || !callerUpdatesMainMeta) {
2888
+ return false;
2889
+ }
2890
+ const hasChangedPrice = (nextValue, currentValue) => {
2891
+ if (nextValue === undefined || nextValue === null || nextValue === '') {
2892
+ return false;
2893
+ }
2894
+ return !new _decimal.default(Number(nextValue) || 0).equals(new _decimal.default(Number(currentValue) || 0));
2895
+ };
2896
+ return hasChangedPrice(updates.selling_price, targetProduct.selling_price) || hasChangedPrice(updates.original_price, targetProduct.original_price);
2897
+ })();
2898
+ if (isPersistedOrderLine && !callerChangesLineConfiguration && !callerUpdatesManualPrice && !callerAllowsBookingReprice) {
2899
+ nextProduct.selling_price = targetProduct.selling_price;
2900
+ nextProduct.original_price = targetProduct.original_price;
2901
+ nextProduct.payment_price = targetProduct.payment_price;
2902
+ nextProduct.metadata = {
2903
+ ...(nextProduct.metadata || {}),
2904
+ source_product_price: targetProduct.metadata?.source_product_price,
2905
+ main_product_selling_price: targetProduct.metadata?.main_product_selling_price,
2906
+ main_product_original_price: targetProduct.metadata?.main_product_original_price,
2907
+ price_schema_version: targetProduct.metadata?.price_schema_version ?? 2
2908
+ };
2909
+ nextProduct.product_bundle = this.preservePersistedBundlePriceFields(targetProduct.product_bundle, nextProduct.product_bundle);
2910
+ } else if (callerChangesLineConfiguration && callerUpdatesSourcePrice && (!callerUpdatesMainSellingPrice || !callerUpdatesMainOriginalPrice)) {
2911
+ // 新配置只传了 source 时,不能把未传的 main_product_* 从旧行继承下来。
2912
+ // 以新 source + 新 option 重建券前价;券后价未显式提供时保留旧主商品折扣额。
2913
+ const previousMainOriginal = new _decimal.default(Number(targetProduct.metadata?.main_product_original_price) || 0);
2914
+ const previousMainSelling = new _decimal.default(Number(targetProduct.metadata?.main_product_selling_price) || previousMainOriginal.toNumber());
2915
+ const preservedDiscount = previousMainOriginal.minus(previousMainSelling);
2916
+ const nextMainOriginal = new _decimal.default(Number(updates.metadata.source_product_price) || 0).plus((0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(nextProduct)));
2917
+ nextProduct.metadata = {
2918
+ ...(nextProduct.metadata || {}),
2919
+ source_product_price: String(updates.metadata.source_product_price),
2920
+ main_product_original_price: callerUpdatesMainOriginalPrice ? String(updates.metadata.main_product_original_price) : nextMainOriginal.toDecimalPlaces(2).toFixed(2),
2921
+ main_product_selling_price: callerUpdatesMainSellingPrice ? String(updates.metadata.main_product_selling_price) : nextMainOriginal.minus(preservedDiscount).toDecimalPlaces(2).toFixed(2),
2922
+ price_schema_version: 2
2923
+ };
2924
+ } else if (callerChangesVariant && !callerUpdatesTopPrice && !callerUpdatesMainMeta) {
2925
+ // variant 已变化但 caller 没有显式报价时,旧 v2 metadata 不能继续充当权威价格源。
2926
+ // 清理后由 normalize 优先读取 metadata.origin.variant 中新 variant 的目录价;
2927
+ // 顶层 original/selling 的旧差额仍会作为 legacy discount 迁移到新价格。
2928
+ const existedMeta = nextProduct.metadata || {};
2929
+ nextProduct.metadata = {
2930
+ ...existedMeta
2931
+ };
2932
+ delete nextProduct.metadata.source_product_price;
2933
+ delete nextProduct.metadata.main_product_selling_price;
2934
+ delete nextProduct.metadata.main_product_original_price;
2935
+ delete nextProduct.metadata.price_schema_version;
2936
+ } else if (callerChangesLineConfiguration && !callerUpdatesTopPrice && !callerUpdatesMainMeta) {
2937
+ // 仅 option / bundle 变化时,保留历史 source 和主商品折扣金额,
2938
+ // 但必须让 original 与 selling 一起随新的 option 金额移动。
2939
+ const previousOptionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(targetProduct));
2940
+ const nextOptionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(nextProduct));
2941
+ const previousMainOriginal = new _decimal.default(Number(targetProduct.metadata?.main_product_original_price ?? targetProduct.original_price ?? 0) || 0);
2942
+ const previousMainSelling = new _decimal.default(Number(targetProduct.metadata?.main_product_selling_price ?? targetProduct.selling_price ?? previousMainOriginal.toNumber()) || 0);
2943
+ const preservedDiscount = previousMainOriginal.minus(previousMainSelling);
2944
+ const previousSource = targetProduct.metadata?.source_product_price != null ? new _decimal.default(Number(targetProduct.metadata.source_product_price) || 0) : previousMainOriginal.minus(previousOptionSum);
2945
+ const nextMainOriginal = previousSource.plus(nextOptionSum);
2946
+ nextProduct.metadata = {
2947
+ ...(nextProduct.metadata || {}),
2948
+ source_product_price: previousSource.toDecimalPlaces(2).toFixed(2),
2949
+ main_product_original_price: nextMainOriginal.toDecimalPlaces(2).toFixed(2),
2950
+ main_product_selling_price: nextMainOriginal.minus(preservedDiscount).toDecimalPlaces(2).toFixed(2),
2951
+ price_schema_version: 2
2952
+ };
2953
+ } else if (callerUpdatesTopPrice && !callerUpdatesMainMeta) {
2954
+ const existedMeta = nextProduct.metadata || {};
2955
+ nextProduct.metadata = {
2956
+ ...existedMeta
2957
+ };
2958
+ delete nextProduct.metadata.source_product_price;
2959
+ delete nextProduct.metadata.main_product_selling_price;
2960
+ delete nextProduct.metadata.main_product_original_price;
2961
+ delete nextProduct.metadata.price_schema_version;
2962
+ }
2963
+ // normalizeOrderProduct 幂等:v2 metadata 存在 → 保留 main_product_* 折扣重算 composite;
2964
+ // 否则 → 把顶层 selling_price 视作 source,派生 main_product_* 并写 price_schema_version: 2。
2965
+ let normalizedProduct = (0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct));
2966
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
2967
+ const preservedBookingUid = nextProduct.booking_uid || nextProduct.metadata?.booking_uid || targetProduct.booking_uid || targetProduct.metadata?.booking_uid;
2968
+ if (!booking && preservedBookingUid) {
2969
+ normalizedProduct.booking_uid = preservedBookingUid;
2970
+ normalizedProduct.metadata = {
2971
+ ...(normalizedProduct.metadata || {}),
2972
+ booking_uid: preservedBookingUid
2973
+ };
2974
+ }
2975
+ if (booking) {
2976
+ const productUid = String(normalizedProduct.metadata?.unique_identification_number || targetProduct.metadata?.unique_identification_number || unique_identification_number || (0, _utils.createUuidV4)());
2977
+ normalizedProduct.metadata = {
2978
+ ...(normalizedProduct.metadata || {}),
2979
+ unique_identification_number: productUid
2980
+ };
2981
+ const linked = this.createLinkedProductAndBooking({
2982
+ product: {
2983
+ ...normalizedProduct,
2984
+ unique_identification_number: productUid
2985
+ },
2986
+ booking
2987
+ });
2988
+ this.removeLinkedBookingsForProduct(tempOrder, targetProduct);
2989
+ normalizedProduct = {
2990
+ ...normalizedProduct,
2991
+ booking_uid: linked.bookingUid,
2992
+ metadata: {
2993
+ ...(normalizedProduct.metadata || {}),
2994
+ booking_uid: linked.bookingUid,
2995
+ unique_identification_number: productUid
2996
+ }
2997
+ };
2998
+ tempOrder.bookings = [...(tempOrder.bookings || []), linked.booking];
2999
+ }
3000
+ tempOrder.products[productIndex] = normalizedProduct;
3001
+ this.syncTempOrderHolderFromBookings(tempOrder);
3002
+ this.captureProductRuntime(tempOrder, updates, tempOrder.products[productIndex], tempOrder.products[productIndex].metadata?.unique_identification_number);
3003
+ return tempOrder.products[productIndex].metadata?.unique_identification_number;
3004
+ }
3005
+ async updateOrderProduct(params) {
3006
+ const tempOrder = this.ensureTempOrder();
3007
+ const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
3008
+ await this.applyPromotion();
3009
+ this.applyDiscount({
3010
+ reapplyBookingDiscountProductUids: params.allow_booking_discount_reapply && updatedProductUid ? [String(updatedProductUid)] : undefined
3011
+ });
3012
+ this.sanitizeTempOrderProducts(tempOrder);
3013
+ await this.recalculateSummary({
3014
+ createIfMissing: true
3015
+ });
3016
+ this.persistTempOrder();
3017
+ this.logInfo('updateOrderProduct success', {
3018
+ params
3019
+ });
3020
+ return tempOrder.products;
3021
+ }
3022
+ async updateOrderProducts(paramsList) {
3023
+ const tempOrder = this.ensureTempOrder();
3024
+ if (!paramsList.length) {
3025
+ return tempOrder.products;
3026
+ }
3027
+ const reapplyBookingDiscountProductUids = paramsList.reduce((acc, params) => {
3028
+ const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
3029
+ // 批量改预约时间时,延迟到最后一次折扣计算里统一恢复已选预约折扣卡。
3030
+ if (params.allow_booking_discount_reapply && updatedProductUid) {
3031
+ acc.push(String(updatedProductUid));
3032
+ }
3033
+ return acc;
3034
+ }, []);
3035
+ await this.finalizeAfterProductsMutation(tempOrder, {
3036
+ reapplyBookingDiscountProductUids: reapplyBookingDiscountProductUids.length ? reapplyBookingDiscountProductUids : undefined
3037
+ });
3038
+ this.logInfo('updateOrderProduct success', {
3039
+ paramsList
3040
+ });
3041
+ return tempOrder.products;
3042
+ }
3043
+ async updateOrderProductQuantity(params) {
3044
+ const {
3045
+ product_id,
3046
+ product_variant_id,
3047
+ num,
3048
+ unique_identification_number,
3049
+ identity_key,
3050
+ product_sku,
3051
+ product_bundle
3052
+ } = params;
3053
+ const tempOrder = this.ensureTempOrder();
521
3054
  const identityLookup = {
522
3055
  product_id,
523
3056
  product_variant_id
524
3057
  };
3058
+ if (unique_identification_number !== undefined) {
3059
+ identityLookup.unique_identification_number = unique_identification_number;
3060
+ if (identity_key === undefined) {
3061
+ identityLookup.identity_key = unique_identification_number;
3062
+ }
3063
+ }
525
3064
  if (identity_key !== undefined) identityLookup.identity_key = identity_key;
526
- if (product_option_item !== undefined) identityLookup.product_option_item = product_option_item;
3065
+ if (product_sku !== undefined) identityLookup.product_sku = product_sku;
527
3066
  if (product_bundle !== undefined) identityLookup.product_bundle = product_bundle;
528
- const productIndex = (0, _utils3.getProductIdentityIndex)(tempOrder.products, identityLookup);
3067
+ let productIndex = -1;
3068
+ if (unique_identification_number !== undefined) {
3069
+ const targetUid = String(unique_identification_number);
3070
+ productIndex = tempOrder.products.findIndex(item => {
3071
+ return String(item.metadata?.unique_identification_number || item.unique_identification_number || '') === targetUid;
3072
+ });
3073
+ }
529
3074
  if (productIndex === -1) {
530
- throw new Error('[Order] 目标商品不存在,无法更新');
3075
+ productIndex = (0, _utils3.getProductIdentityIndex)(tempOrder.products, identityLookup);
3076
+ }
3077
+ if (productIndex === -1) {
3078
+ throw new Error('[Order] 目标商品不存在,无法更新数量');
531
3079
  }
532
3080
  const targetProduct = tempOrder.products[productIndex];
533
- const nextProduct = {
3081
+ const normalizedProduct = (0, _utils3.normalizeOrderProduct)({
534
3082
  ...targetProduct,
3083
+ num: (0, _utils3.getSafeProductNum)(num),
3084
+ metadata: {
3085
+ ...(targetProduct.metadata || {}),
3086
+ unique_identification_number: targetProduct.metadata?.unique_identification_number || unique_identification_number
3087
+ }
3088
+ });
3089
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3090
+ tempOrder.products[productIndex] = normalizedProduct;
3091
+ await this.applyPromotion();
3092
+ this.applyDiscount();
3093
+ this.sanitizeTempOrderProducts(tempOrder);
3094
+ await this.recalculateSummary({
3095
+ createIfMissing: true
3096
+ });
3097
+ this.persistTempOrder();
3098
+ this.logInfo('updateOrderProductQuantity success', {
3099
+ params
3100
+ });
3101
+ return tempOrder.products;
3102
+ }
3103
+ updateOrderBooking(params) {
3104
+ const {
3105
+ unique_identification_number,
3106
+ updates
3107
+ } = params;
3108
+ const tempOrder = this.ensureTempOrder();
3109
+ const bookingUid = String(unique_identification_number || '');
3110
+ const bookingIndex = this.findBookingIndexByUniqueIdentificationNumber(tempOrder, bookingUid);
3111
+ if (!bookingUid || bookingIndex === -1) {
3112
+ throw new Error('[Order] 目标 booking 不存在,无法更新');
3113
+ }
3114
+ const targetBooking = tempOrder.bookings[bookingIndex] || {};
3115
+ const nextBooking = {
3116
+ ...targetBooking,
535
3117
  ...updates,
536
- product_id,
537
- product_variant_id
3118
+ metadata: {
3119
+ ...(targetBooking.metadata || {}),
3120
+ ...(updates.metadata || {}),
3121
+ unique_identification_number: bookingUid
3122
+ }
538
3123
  };
539
- nextProduct.num = (0, _utils3.getSafeProductNum)(nextProduct.num);
540
- // 兜底:caller 通过 updates 直接改了 selling_price / original_price 时,
541
- // 视为 "重新给 source-level 主价"(caller 侧旧约定仍是 main-only 单价)。
542
- // 清掉 metadata 里的 source_product_price / main_product_* / price_schema_version,
543
- // 让 normalize 把顶层 selling_price 当作 source 来重算 main_original(=source+options) /
544
- // main_selling / composite;否则旧 metadata 会反压顶层新值,触发口径不一致。
545
- // 如果 caller 走推荐路径(updates.metadata.* 显式指定主价),保留 metadata 由 normalize 认它。
546
- const callerUpdatesTopPrice = Object.prototype.hasOwnProperty.call(updates || {}, 'selling_price') || Object.prototype.hasOwnProperty.call(updates || {}, 'original_price');
547
- const callerUpdatesMainMeta = updates?.metadata?.main_product_selling_price !== undefined || updates?.metadata?.main_product_original_price !== undefined || updates?.metadata?.source_product_price !== undefined;
548
- if (callerUpdatesTopPrice && !callerUpdatesMainMeta) {
549
- const existedMeta = nextProduct.metadata || {};
550
- nextProduct.metadata = {
551
- ...existedMeta
552
- };
553
- delete nextProduct.metadata.source_product_price;
554
- delete nextProduct.metadata.main_product_selling_price;
555
- delete nextProduct.metadata.main_product_original_price;
556
- delete nextProduct.metadata.price_schema_version;
557
- }
558
- // normalizeOrderProduct 幂等:v2 metadata 存在 → 保留 main_product_* 折扣重算 composite;
559
- // 否则 → 把顶层 selling_price 视作 source,派生 main_product_* 并写 price_schema_version: 2。
560
- tempOrder.products[productIndex] = (0, _utils3.normalizeOrderProduct)(nextProduct);
561
- this.applyDiscount();
3124
+ this.syncBookingMetadataHolder(nextBooking, nextBooking.metadata);
3125
+ nextBooking.is_all = (0, _utils.normalizeBookingIsAll)(nextBooking);
3126
+ nextBooking.sub_type = (0, _utils.normalizeBookingSubType)(nextBooking);
3127
+ tempOrder.bookings[bookingIndex] = nextBooking;
3128
+ this.syncTempOrderHolderFromBookings(tempOrder);
3129
+ this.persistTempOrder();
3130
+ return tempOrder.bookings;
3131
+ }
3132
+
3133
+ /**
3134
+ * 商品行写路径收尾:促销 → 折扣 → 清洗 → summary → 持久化。
3135
+ * 批量删除等多行变更场景只应调用一次,避免 N 次 applyPromotion。
3136
+ *
3137
+ * @example
3138
+ * await this.finalizeAfterProductsMutation(tempOrder);
3139
+ */
3140
+ async finalizeAfterProductsMutation(tempOrder, options) {
3141
+ this.syncTempOrderHolderFromBookings(tempOrder);
3142
+ await this.applyPromotion();
3143
+ this.applyDiscount({
3144
+ reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
3145
+ });
3146
+ this.sanitizeTempOrderProducts(tempOrder);
562
3147
  await this.recalculateSummary({
563
3148
  createIfMissing: true
564
3149
  });
565
3150
  this.persistTempOrder();
3151
+ }
3152
+
3153
+ /**
3154
+ * 批量按 identity 删除购物车行,末尾仅一次促销重算。
3155
+ *
3156
+ * @example
3157
+ * await order.removeProductsFromOrder([identityA, identityB]);
3158
+ */
3159
+ async removeProductsFromOrder(identities) {
3160
+ const tempOrder = this.ensureTempOrder();
3161
+ if (!identities.length) {
3162
+ return tempOrder.products;
3163
+ }
3164
+ const productsBeforeRemove = tempOrder.products || [];
3165
+ const bookingsBeforeRemove = tempOrder.bookings || [];
3166
+ const matchedProducts = productsBeforeRemove.filter(item => {
3167
+ return identities.some(identity => (0, _utils3.isIdentityMatch)(item, identity));
3168
+ });
3169
+ const removedProducts = [];
3170
+ tempOrder.products = tempOrder.products.filter(item => {
3171
+ const shouldRemove = identities.some(identity => (0, _utils3.isIdentityMatch)(item, identity));
3172
+ if (shouldRemove) removedProducts.push(item);
3173
+ return !shouldRemove;
3174
+ });
3175
+ for (const product of removedProducts) {
3176
+ this.removeLinkedBookingsForProduct(tempOrder, product);
3177
+ }
3178
+ const linkedBookingUidsToRemove = new Set();
3179
+ for (const product of matchedProducts) {
3180
+ const productUid = product?.metadata?.unique_identification_number || product?.unique_identification_number;
3181
+ const productBookingUid = product?.booking_uid || product?.metadata?.booking_uid;
3182
+ const isAddTimeProduct = product?.metadata?.product_add_schedule_time !== undefined && product?.metadata?.product_add_schedule_time !== null;
3183
+ if (isAddTimeProduct) continue;
3184
+ if (productBookingUid !== undefined && productBookingUid !== null && productBookingUid !== '') {
3185
+ linkedBookingUidsToRemove.add(String(productBookingUid));
3186
+ }
3187
+ if (productUid !== undefined && productUid !== null && productUid !== '') {
3188
+ bookingsBeforeRemove.forEach(booking => {
3189
+ const bookingUid = this.getBookingUniqueIdentificationNumber(booking);
3190
+ if (bookingUid && String(booking?.product_uid || '') === String(productUid)) {
3191
+ linkedBookingUidsToRemove.add(bookingUid);
3192
+ }
3193
+ });
3194
+ }
3195
+ }
3196
+ if (linkedBookingUidsToRemove.size > 0) {
3197
+ // 删除主预约商品时,同步移除挂在同一 booking_uid 下的加时商品,避免残留孤儿行。
3198
+ tempOrder.products = (tempOrder.products || []).filter(line => {
3199
+ const lineBookingUid = line?.metadata?.booking_uid || line?.booking_uid;
3200
+ const isAddTimeProduct = line?.metadata?.product_add_schedule_time !== undefined && line?.metadata?.product_add_schedule_time !== null;
3201
+ return !(isAddTimeProduct && lineBookingUid !== undefined && lineBookingUid !== null && linkedBookingUidsToRemove.has(String(lineBookingUid)));
3202
+ });
3203
+ }
3204
+ await this.finalizeAfterProductsMutation(tempOrder);
3205
+ this.logInfo('removeProductsFromOrder success', {
3206
+ identities
3207
+ });
566
3208
  return tempOrder.products;
567
3209
  }
568
3210
  async removeProductFromOrder(identity) {
3211
+ return this.removeProductsFromOrder([identity]);
3212
+ }
3213
+
3214
+ /**
3215
+ * 仅清空购物车行(products / bookings),不重置整单。
3216
+ * 用于「仅清空商品」;客户、备注、支付等协议字段保持不变。
3217
+ */
3218
+ async clearOrderCartLines() {
569
3219
  const tempOrder = this.ensureTempOrder();
570
- tempOrder.products = tempOrder.products.filter(item => !(0, _utils3.isIdentityMatch)(item, identity));
3220
+ tempOrder.products = [];
3221
+ tempOrder.bookings = [];
3222
+ this.syncTempOrderHolderFromBookings(tempOrder);
3223
+ if (tempOrder._extend && typeof tempOrder._extend === 'object') {
3224
+ tempOrder._extend = {
3225
+ ...tempOrder._extend,
3226
+ productsByUid: {}
3227
+ };
3228
+ }
3229
+ await this.applyPromotion();
571
3230
  this.applyDiscount();
3231
+ this.sanitizeTempOrderProducts(tempOrder);
572
3232
  await this.recalculateSummary({
573
3233
  createIfMissing: true
574
3234
  });
575
3235
  this.persistTempOrder();
576
- return tempOrder.products;
3236
+ this.logInfo('clearOrderCartLines success', {});
3237
+ return tempOrder;
577
3238
  }
578
3239
 
579
3240
  // ─── TempOrder: 提交 ───
580
3241
 
3242
+ buildCurrentSubmitPayloadForLocalPrint(params) {
3243
+ const tempOrder = this.ensureTempOrder();
3244
+ this.persistTempOrder();
3245
+ const payload = (0, _utils.buildSubmitPayload)({
3246
+ tempOrder,
3247
+ cacheId: params?.cacheId ?? this.cacheId,
3248
+ platform: params?.platform || this.otherParams?.platform,
3249
+ businessCode: params?.businessCode ?? this.otherParams?.businessCode ?? this.otherParams?.business_code,
3250
+ channel: params?.channel,
3251
+ type: params?.type,
3252
+ summary: this.store.summary || (0, _utils.createEmptySummary)(),
3253
+ enhance: nextPayload => ({
3254
+ ...nextPayload,
3255
+ order_number: tempOrder.order_number ?? nextPayload.order_number ?? null,
3256
+ shop_order_number: tempOrder.shop_order_number ?? nextPayload.shop_order_number ?? null,
3257
+ shop_full_order_number: tempOrder.shop_full_order_number ?? nextPayload.shop_full_order_number ?? null,
3258
+ small_ticket_data_flag: 1
3259
+ })
3260
+ });
3261
+ payload.created_at = undefined;
3262
+ return payload;
3263
+ }
3264
+ async applyLocalPrintOrderNumbers(order) {
3265
+ const tempOrder = this.ensureTempOrder();
3266
+ if (!order || typeof order !== 'object') return tempOrder;
3267
+ const shopOrderNumber = order.shop_order_number;
3268
+ const shopFullOrderNumber = order.shop_full_order_number;
3269
+ if (shopOrderNumber !== undefined && shopOrderNumber !== null && shopOrderNumber !== '') {
3270
+ tempOrder.shop_order_number = String(shopOrderNumber);
3271
+ }
3272
+ if (shopFullOrderNumber !== undefined && shopFullOrderNumber !== null && shopFullOrderNumber !== '') {
3273
+ tempOrder.shop_full_order_number = String(shopFullOrderNumber);
3274
+ }
3275
+ this.persistTempOrder();
3276
+ await this.saveDraft();
3277
+ return tempOrder;
3278
+ }
3279
+
3280
+ /**
3281
+ * 提交当前 Sales tempOrder。
3282
+ *
3283
+ * smallTicketDataFlag 用于 B 端 checkout 小票数据返回。PaymentModal 在支付提交时
3284
+ * 应传 1,确保 /shop/order/sales/checkout 返回打印所需数据。
3285
+ */
581
3286
  async submitTempOrder(params) {
3287
+ return this.runSubmitTempOrder(params);
3288
+ }
3289
+ async submitTempOrderAsync(params) {
3290
+ return this.runSubmitTempOrder({
3291
+ ...params,
3292
+ syncMode: true
3293
+ });
3294
+ }
3295
+ async runSubmitTempOrder(params) {
582
3296
  const tempOrder = this.ensureTempOrder();
3297
+ const shouldConfirmPendingVoucherPayments = params?.confirmPendingVoucherPayments !== false;
3298
+ const hasPaymentOverride = params?.payments !== undefined;
3299
+ const preparedPaymentOverride = hasPaymentOverride ? this.prepareVoucherPaymentsForSubmit(params?.payments || [], shouldConfirmPendingVoucherPayments) : undefined;
3300
+ if (!hasPaymentOverride) {
3301
+ if (shouldConfirmPendingVoucherPayments) {
3302
+ this.confirmPendingVoucherPayments({
3303
+ persist: false,
3304
+ recalculateSummary: false
3305
+ });
3306
+ } else {
3307
+ this.discardPendingVoucherPayments({
3308
+ persist: false,
3309
+ recalculateSummary: false
3310
+ });
3311
+ }
3312
+ }
583
3313
  this.persistTempOrder();
3314
+ const latestSummary = (await this.recalculateSummary({
3315
+ createIfMissing: true
3316
+ })) || this.store.summary || (0, _utils.createEmptySummary)();
584
3317
  const effectiveCacheId = params?.cacheId ?? this.cacheId;
3318
+ const hasPaymentStatusOverride = params?.paymentStatus !== undefined;
3319
+ const hasSmallTicketDataFlagOverride = params?.smallTicketDataFlag !== undefined;
3320
+ const enhancePayload = hasPaymentOverride || hasPaymentStatusOverride || hasSmallTicketDataFlagOverride || params?.enhancePayload ? (payload, ctx) => {
3321
+ const nextPayload = {
3322
+ ...payload,
3323
+ ...(hasPaymentOverride ? {
3324
+ payments: preparedPaymentOverride || []
3325
+ } : {}),
3326
+ ...(hasPaymentStatusOverride && !shouldConfirmPendingVoucherPayments ? {
3327
+ payment_status: params?.paymentStatus
3328
+ } : {}),
3329
+ ...(hasSmallTicketDataFlagOverride ? {
3330
+ small_ticket_data_flag: params?.smallTicketDataFlag
3331
+ } : {})
3332
+ };
3333
+ const enhancedPayload = params?.enhancePayload ? params.enhancePayload(nextPayload, ctx) : nextPayload;
3334
+ return enhancedPayload;
3335
+ } : undefined;
3336
+ const submitSnapshot = this.getLastOrderSubmitSnapshot();
585
3337
  const payload = (0, _utils.buildSubmitPayload)({
586
3338
  tempOrder,
587
3339
  cacheId: effectiveCacheId,
588
- platform: params?.platform,
589
- businessCode: params?.businessCode,
3340
+ platform: params?.platform || this.otherParams?.platform,
3341
+ businessCode: submitSnapshot.businessCode ?? params?.businessCode ?? this.otherParams?.businessCode ?? this.otherParams?.business_code,
590
3342
  channel: params?.channel,
591
- type: params?.type,
592
- enhance: params?.enhancePayload
3343
+ type: submitSnapshot.type ?? params?.type,
3344
+ summary: latestSummary,
3345
+ request_unique_idempotency_token: params?.request_unique_idempotency_token,
3346
+ enhance: enhancePayload
593
3347
  });
3348
+ if (params?.syncMode) {
3349
+ payload.sync_mode = true;
3350
+ }
3351
+ // TODO: 前端生成的时间现在是有问题的,后面要统一优化
3352
+ if (!payload.created_at) {
3353
+ payload.created_at = (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss');
3354
+ }
3355
+ await this.saveDraft();
594
3356
  let result;
595
- if (tempOrder.order_id) {
596
- const products = (0, _utils.filterProductsForScanOrderMore)(payload.products);
597
- const moreResult = await this.scanOrderMore({
598
- query: {
599
- order_id: tempOrder.order_id,
600
- products: products,
601
- request_unique_idempotency_token: payload.request_unique_idempotency_token
602
- }
3357
+ try {
3358
+ this.logInfo('submitTempOrder calling sales checkout', {
3359
+ orderId: payload.order_id,
3360
+ externalSaleNumber: payload.external_sale_number,
3361
+ requestUniqueIdempotencyToken: payload.request_unique_idempotency_token,
3362
+ paymentStatus: payload.payment_status,
3363
+ paymentsCount: payload.payments?.length || 0,
3364
+ smallTicketDataFlag: payload.small_ticket_data_flag,
3365
+ syncMode: payload.sync_mode
603
3366
  });
604
- if (moreResult.code === 200) {
605
- result = {
606
- data: {
607
- order_id: tempOrder.order_id
608
- }
609
- };
610
- }
611
- } else {
612
- result = await this.submitScanOrder({
3367
+ result = await this.submitSalesOrder({
613
3368
  query: payload
614
3369
  });
3370
+ } catch (error) {
3371
+ const backendErrorResponse = this.extractSubmitErrorResponse(error);
3372
+ if (backendErrorResponse) {
3373
+ this.store.syncState = 'failed';
3374
+ this.logSubmitBackendRejected(backendErrorResponse, payload);
3375
+ return backendErrorResponse;
3376
+ }
3377
+ this.store.syncState = 'failed';
3378
+ this.logError('submitTempOrder failed', {
3379
+ error: error instanceof Error ? error.message : String(error),
3380
+ orderId: payload.order_id,
3381
+ externalSaleNumber: payload.external_sale_number,
3382
+ paymentStatus: payload.payment_status,
3383
+ paymentsCount: payload.payments?.length || 0
3384
+ });
3385
+ throw error;
3386
+ }
3387
+ if (this.isSubmitResponseRejected(result)) {
3388
+ this.store.syncState = 'failed';
3389
+ this.logSubmitBackendRejected(result, payload);
3390
+ return result;
3391
+ }
3392
+ const submittedOrderId = this.extractOrderIdFromSubmitResult(result);
3393
+ this.logInfo('runSubmitTempOrder: 提交成功', {
3394
+ submittedOrderId,
3395
+ result,
3396
+ tempOrder: {
3397
+ ...tempOrder,
3398
+ _extend: undefined
3399
+ }
3400
+ });
3401
+ if (submittedOrderId !== null && submittedOrderId !== undefined) {
3402
+ tempOrder.order_id = submittedOrderId;
3403
+ const resultRecord = result;
3404
+ await this.markLocalOrderSynced(submittedOrderId, resultRecord?.data || resultRecord || {});
3405
+ } else if (this.isLocalPendingSubmitResult(result)) {
3406
+ this.store.syncState = 'local';
3407
+ } else {
3408
+ this.store.syncState = 'submitted';
615
3409
  }
616
3410
  return result;
617
3411
  }
3412
+ async markLocalOrderSynced(orderId, remoteOrder) {
3413
+ const tempOrder = this.ensureTempOrder();
3414
+ tempOrder.order_id = orderId;
3415
+ this.applyRemoteOrderNumbersToTempOrder(tempOrder, remoteOrder);
3416
+ this.store.lastOrderInfo = remoteOrder;
3417
+ this.store.syncState = 'submitted';
3418
+ this.persistTempOrder();
3419
+ await this.saveDraft();
3420
+ }
3421
+ applyRemoteOrderNumbersToTempOrder(tempOrder, remoteOrder) {
3422
+ const applyOrderNumber = key => {
3423
+ const value = remoteOrder?.[key];
3424
+ if (value !== undefined && value !== null && value !== '') {
3425
+ tempOrder[key] = String(value);
3426
+ }
3427
+ };
3428
+ applyOrderNumber('order_number');
3429
+ applyOrderNumber('shop_order_number');
3430
+ applyOrderNumber('shop_full_order_number');
3431
+ }
3432
+ isSubmitResponseRejected(response) {
3433
+ const isErrorCode = response?.code !== undefined && response.code !== 200 && response.code !== '200';
3434
+ const isErrorStatus = response?.status === false || typeof response?.status === 'number' && response.status >= 400;
3435
+ const isErrorSuccess = response?.success === false;
3436
+ return isErrorCode || isErrorStatus || isErrorSuccess;
3437
+ }
3438
+ isLocalPendingSubmitResult(response) {
3439
+ const data = response?.data && typeof response.data === 'object' ? response.data : response;
3440
+ return Number(data?.need_sync) === 1;
3441
+ }
3442
+ isSubmitErrorResponse(response) {
3443
+ if (!response || typeof response !== 'object') return false;
3444
+ return this.isSubmitResponseRejected(response) || response.message !== undefined || response.msg !== undefined || response.error !== undefined;
3445
+ }
3446
+ extractSubmitErrorResponse(error) {
3447
+ const candidates = [error?.response?.data, error?.data, error?.response?.body, error?.body];
3448
+ return candidates.find(candidate => this.isSubmitErrorResponse(candidate)) || null;
3449
+ }
3450
+ logSubmitBackendRejected(response, payload) {
3451
+ this.logError('submitTempOrder backend rejected', {
3452
+ response,
3453
+ payload: {
3454
+ order_id: payload.order_id,
3455
+ external_sale_number: payload.external_sale_number,
3456
+ payment_status: payload.payment_status,
3457
+ paymentsCount: payload.payments?.length || 0
3458
+ }
3459
+ });
3460
+ }
3461
+ generateIdempotencyToken() {
3462
+ const tempOrder = this.ensureTempOrder();
3463
+ const token = this.buildPaymentSyncIdempotencyToken(tempOrder, tempOrder.payments);
3464
+ return `${token}_${Date.now()}`;
3465
+ }
3466
+ async syncPaymentsToOrder(params) {
3467
+ const tempOrder = this.ensureTempOrder();
3468
+ const mappedPayments = (0, _utils.mapPaymentItemsToOrderPayments)(params.payments || []);
3469
+ const shouldConfirmPendingVoucherPayments = params.confirmPendingVoucherPayments ?? params.submitWhenPaid === true;
3470
+ const effectivePayments = shouldConfirmPendingVoucherPayments ? this.confirmPendingVoucherPaymentsInList(mappedPayments) : params.confirmPendingVoucherPayments === false ? this.discardPendingVoucherPaymentsFromList(mappedPayments) : mappedPayments;
3471
+ const completion = this.getPaymentCompletion(effectivePayments);
3472
+ const orderPaidPaymentTotal = this.calculateOrderPaidPaymentTotal(effectivePayments);
3473
+ const paymentStatus = completion.isOrderFullyPaid ? params.paymentStatus || 'paid' : orderPaidPaymentTotal.gt(0) ? 'partially_paid' : 'payment_processing';
3474
+ tempOrder.payments = effectivePayments;
3475
+ tempOrder.payment_status = paymentStatus;
3476
+ this.persistTempOrder();
3477
+ await this.saveDraft();
3478
+ if (!params.submitWhenPaid) {
3479
+ return completion;
3480
+ }
3481
+ if (this.store.syncState === 'submitting') {
3482
+ return completion;
3483
+ }
3484
+ this.store.syncState = 'submitting';
3485
+ const paymentSyncIdempotencyToken = this.buildPaymentSyncIdempotencyToken(tempOrder, effectivePayments);
3486
+ const submitParams = {
3487
+ payments: effectivePayments,
3488
+ paymentStatus,
3489
+ smallTicketDataFlag: params.smallTicketDataFlag,
3490
+ businessCode: params.businessCode,
3491
+ channel: params.channel,
3492
+ confirmPendingVoucherPayments: true,
3493
+ enhancePayload: payload => {
3494
+ const nextPayload = {
3495
+ ...payload,
3496
+ request_unique_idempotency_token: paymentSyncIdempotencyToken
3497
+ };
3498
+ return nextPayload;
3499
+ }
3500
+ };
3501
+ const submitResult = params.checkoutSyncTaskEnabled === false ? await this.submitTempOrderAsync(submitParams) : await this.submitTempOrder(submitParams);
3502
+ return {
3503
+ ...completion,
3504
+ submitResult
3505
+ };
3506
+ }
618
3507
  createOrder(params) {
619
3508
  const order = {
620
3509
  type: params?.type || 'appointment_booking',
@@ -642,6 +3531,7 @@ class OrderModule extends _BaseModule.BaseModule {
642
3531
  item._origin.duration = duration;
643
3532
  item._origin.sub_type = durationType;
644
3533
  }
3534
+ item._origin.sub_type = (0, _utils.normalizeBookingSubType)(item._origin);
645
3535
  const discountList = (0, _utils.getAllDiscountList)(item);
646
3536
  item._origin.product.discount_list = discountList;
647
3537
 
@@ -718,14 +3608,54 @@ class OrderModule extends _BaseModule.BaseModule {
718
3608
  });
719
3609
  return this.request.post(fetchUrl, params);
720
3610
  }
3611
+ async cancelOrder(params) {
3612
+ if (!Array.isArray(params.order_ids) || params.order_ids.length === 0) {
3613
+ throw new Error('取消订单失败:order_ids 不能为空');
3614
+ }
3615
+ const payload = {
3616
+ order_ids: params.order_ids,
3617
+ status: 'cancelled',
3618
+ change_schedule_event_appointment_status: 1,
3619
+ is_void: 1
3620
+ };
3621
+ this.logInfo('cancelOrder called', {
3622
+ url: '/order/update-status',
3623
+ method: 'PUT',
3624
+ orderIdsCount: params.order_ids.length
3625
+ });
3626
+ return this.request.put('/order/update-status', payload, {
3627
+ isShopApi: true
3628
+ });
3629
+ }
3630
+ async changeBookingStatus(params) {
3631
+ if (!params.booking_id) {
3632
+ throw new Error('变更预约状态失败:booking_id 不能为空');
3633
+ }
3634
+ if (!params.appointment_status) {
3635
+ throw new Error('变更预约状态失败:appointment_status 不能为空');
3636
+ }
3637
+ const url = `/schedule/booking/appointment-status/${params.booking_id}`;
3638
+ const payload = {
3639
+ appointment_status: params.appointment_status
3640
+ };
3641
+ this.logInfo('changeBookingStatus called', {
3642
+ url,
3643
+ method: 'PUT',
3644
+ bookingId: params.booking_id,
3645
+ appointmentStatus: params.appointment_status
3646
+ });
3647
+ return this.request.put(url, payload, {
3648
+ isShopApi: true
3649
+ });
3650
+ }
721
3651
 
722
3652
  /**
723
3653
  * Checkout 专用:创建订单到后端
724
- *
3654
+ *
725
3655
  * 专门为 Checkout 模块设计的订单创建方法,
726
- * 直接调用 /order/checkout 接口创建订单
3656
+ * 通过 OS Server 调用 checkout 接口创建订单
727
3657
  * 接收已经处理好的订单数据,无需再处理购物车
728
- *
3658
+ *
729
3659
  * @param params Checkout 订单参数(已处理的订单数据)
730
3660
  * @returns 后端返回的订单数据(包含订单ID等)
731
3661
  */
@@ -774,7 +3704,7 @@ class OrderModule extends _BaseModule.BaseModule {
774
3704
  }
775
3705
  // 记录接口调用详情
776
3706
  this.logInfo('Calling backend checkout API', {
777
- url: '/order/checkout',
3707
+ url: '/shop/order/checkout',
778
3708
  orderType: orderData.type,
779
3709
  platform: orderData.platform,
780
3710
  isDeposit: orderData.is_deposit,
@@ -796,9 +3726,10 @@ class OrderModule extends _BaseModule.BaseModule {
796
3726
  hasOrderId: !!orderData.order_id,
797
3727
  smallTicketDataFlag: orderData.small_ticket_data_flag
798
3728
  });
799
-
800
- // 调用后端接口
801
- const response = await this.request.post('/order/checkout', orderData);
3729
+ const response = await this.request.post('/order/checkout', orderData, {
3730
+ osServer: true,
3731
+ customToast: () => {}
3732
+ });
802
3733
  this.logInfo('Order API called successfully', {
803
3734
  response
804
3735
  });
@@ -811,44 +3742,446 @@ class OrderModule extends _BaseModule.BaseModule {
811
3742
  throw error;
812
3743
  }
813
3744
  }
814
- async submitScanOrder(params) {
3745
+ async submitSalesOrder(params) {
815
3746
  const {
816
3747
  url,
817
3748
  query
818
3749
  } = params;
819
3750
  const fetchUrl = url || '/order/sales/checkout';
820
3751
  return this.request.post(fetchUrl, query, {
3752
+ osServer: true,
821
3753
  customToast: () => {}
822
3754
  });
823
3755
  }
824
- async scanOrderMore(params) {
825
- const {
826
- url,
827
- query
828
- } = params;
829
- const fetchUrl = url || `/order/order/product/${query.order_id}`;
830
- const requestBody = {
831
- products: query.products,
832
- request_unique_idempotency_token: query.request_unique_idempotency_token
833
- };
834
- return this.request.put(fetchUrl, requestBody, {
835
- customToast: () => {}
3756
+
3757
+ /**
3758
+ * 发送支付链接邮件。
3759
+ *
3760
+ * 调用前必须已经有远端 order_id。BaseSales 会在需要时先提交本地订单,
3761
+ * OrderModule 只负责校验 order_ids 并调用 /order/batch-email。
3762
+ */
3763
+ async sendCustomerPayLink(params) {
3764
+ const orderIds = params.order_ids || params.orderIds || [];
3765
+ if (!orderIds.length) {
3766
+ throw new Error('发送支付链接前必须先提交本地订单并取得订单 ID');
3767
+ }
3768
+ if (!params.emails?.length) {
3769
+ throw new Error('发送支付链接需要至少一个邮箱');
3770
+ }
3771
+ return this.request.post('/order/batch-email', {
3772
+ order_ids: orderIds,
3773
+ notify_action: params.notify_action || 'order_payment_reminder',
3774
+ emails: params.emails
836
3775
  });
837
3776
  }
838
-
839
- // TODO 获取详情的接口
840
- async getOrderInfoByRemote(order_id) {
841
- // 读取到后端以后,存在 tempOrder 内的 lastOrderInfo
842
- const res = await this.request.get(`/order/sales/${order_id}`, {
843
- with: ['products', 'scheduleEvents']
3777
+ async getSalesOrderByLookup(params) {
3778
+ const lookup = params.lookup === undefined || params.lookup === null ? '' : String(params.lookup).trim();
3779
+ if (!lookup) {
3780
+ throw new Error('加载销售详情需要 lookup');
3781
+ }
3782
+ // 如果 lookup 是external_sale_number,则forceRemote强制为 false
3783
+ if (lookup.startsWith('LOCAL_')) {
3784
+ params.forceRemote = false;
3785
+ }
3786
+ const res = await this.request.get(`/order/sales/${encodeURIComponent(lookup)}`, {
3787
+ with: ['products', 'bookings', 'payments', 'customer', 'summary', 'contacts_info'],
3788
+ ...(params.forceRemote ? {
3789
+ forceRemote: true
3790
+ } : {})
3791
+ }, {
3792
+ osServer: true
844
3793
  });
845
- if (res.code === 200 && this.store.tempOrder) {
846
- this.store.tempOrder.lastOrderInfo = res.data;
3794
+ if (res.code === 200) {
3795
+ this.store.lastOrderInfo = res.data;
847
3796
  }
848
3797
  return res;
849
3798
  }
3799
+
3800
+ /**
3801
+ * 把 sqlite / 后端返回的订单记录直接恢复为 tempOrder。
3802
+ *
3803
+ * 数据源应尽量返回 tempOrder 所需结构;这里仅做 SDK 运行必须字段兜底,
3804
+ * 例如数组字段、商品行唯一值、价格 metadata、summary 派生状态和 _extend。
3805
+ */
3806
+ async hydrateTempOrderFromRecord(record, options) {
3807
+ const sourceRaw = record && typeof record === 'object' && record.data && record.order_id == null ? record.data : record || {};
3808
+ const identityReadySource = this.seedAnonymousBookingUidsFromProducts(sourceRaw);
3809
+ const normalizedCollections = (0, _orderCollectionIdentity.normalizeOrderCollections)(identityReadySource);
3810
+ const raw = normalizedCollections.order;
3811
+ const hasIdentityChanges = Object.values(normalizedCollections.stats).some(stats => stats.backfilledUidCount > 0 || stats.collapsedDuplicateCount > 0 || stats.uidConflictCount > 0 || stats.anonymousRowCount > 0);
3812
+ if (hasIdentityChanges) {
3813
+ const identityLogMetadata = {
3814
+ collections: normalizedCollections.stats,
3815
+ uidRemapCount: normalizedCollections.uidRemaps.length
3816
+ };
3817
+ const hasIdentityWarnings = Object.values(normalizedCollections.stats).some(stats => stats.uidConflictCount > 0 || stats.anonymousRowCount > 0);
3818
+ if (hasIdentityWarnings) {
3819
+ this.logWarning('Normalized hydrated order collection identities', identityLogMetadata);
3820
+ } else {
3821
+ this.logInfo('Normalized hydrated order collection identities', identityLogMetadata);
3822
+ }
3823
+ }
3824
+ const nextTempOrder = this.normalizeTempOrderForRuntime(raw, {
3825
+ makeEditMark: true
3826
+ });
3827
+ const rawSummary = raw.summary && typeof raw.summary === 'object' ? raw.summary : {};
3828
+ const summarySurcharges = Array.isArray(rawSummary.surcharges) ? rawSummary.surcharges.map(s => ({
3829
+ ...(s || {})
3830
+ })) : (0, _lodashEs.cloneDeep)(nextTempOrder.surcharges || []);
3831
+ this.store.tempOrder = nextTempOrder;
3832
+ this.store.summary = {
3833
+ ...(0, _utils.createEmptySummary)(),
3834
+ ...rawSummary,
3835
+ surcharges: summarySurcharges
3836
+ };
3837
+ nextTempOrder._extend = {
3838
+ ...(nextTempOrder._extend || {}),
3839
+ originalSalesSnapshot: {
3840
+ summary: (0, _lodashEs.cloneDeep)(this.store.summary),
3841
+ products: (0, _lodashEs.cloneDeep)(nextTempOrder.products || []),
3842
+ bookings: (0, _lodashEs.cloneDeep)(nextTempOrder.bookings || []),
3843
+ payments: (0, _lodashEs.cloneDeep)(nextTempOrder.payments || []),
3844
+ surcharges: (0, _lodashEs.cloneDeep)(nextTempOrder.surcharges || []),
3845
+ shop_discount: nextTempOrder.shop_discount || '0.00',
3846
+ productFingerprint: this.buildOrderProductsFingerprint(nextTempOrder.products || [])
3847
+ }
3848
+ };
3849
+ // lastOrderInfo carries the exact backend snapshot used by deposit/type recovery.
3850
+ // Runtime collection repair lives in tempOrder/originalSalesSnapshot and must not
3851
+ // rewrite this protocol snapshot as a side effect of hydration.
3852
+ this.store.lastOrderInfo = sourceRaw.lastOrderInfo || sourceRaw;
3853
+ const hydratedEditDiscounts = this.collectHydratedEditDiscounts(nextTempOrder.products);
3854
+ const currentDiscounts = typeof this.store.discount?.getDiscountList === 'function' ? this.store.discount.getDiscountList() || [] : [];
3855
+ const currentScanDiscounts = currentDiscounts.filter(discount => discount?.isScan);
3856
+ const hydratedDiscountIds = new Set(hydratedEditDiscounts.map(discount => discount?.id));
3857
+ const retainedScanDiscounts = currentScanDiscounts.filter(discount => !hydratedDiscountIds.has(discount?.id));
3858
+ const nextDiscounts = [...hydratedEditDiscounts, ...retainedScanDiscounts];
3859
+ const shouldSkipEmptyDiscountSync = nextDiscounts.length === 0 && currentDiscounts.length === 0;
3860
+ if (!shouldSkipEmptyDiscountSync) {
3861
+ OrderModule.populateSavedAmounts(nextTempOrder.products, nextDiscounts);
3862
+ await this.store.discount?.setOriginalDiscountList(nextDiscounts);
3863
+ await this.store.discount?.setDiscountList(nextDiscounts);
3864
+ }
3865
+ if (options?.recalcOnHydrate) {
3866
+ await this.recalculateSummary({
3867
+ createIfMissing: false
3868
+ });
3869
+ }
3870
+ this.persistTempOrder();
3871
+ return nextTempOrder;
3872
+ }
3873
+
3874
+ /**
3875
+ * 兼容后端详情将 holder 放在 metadata.holder 的历史形态,统一补到运行时展示路径。
3876
+ *
3877
+ * @example
3878
+ * this.normalizeHydratedBookingHolder({ metadata: { holder: { form_record: [1] } } });
3879
+ */
3880
+ normalizeHydratedBookingHolder(booking) {
3881
+ const next = {
3882
+ ...(booking || {})
3883
+ };
3884
+ const metadata = next.metadata && typeof next.metadata === 'object' && !Array.isArray(next.metadata) ? {
3885
+ ...next.metadata
3886
+ } : {};
3887
+ const metadataHolder = metadata.holder && typeof metadata.holder === 'object' && !Array.isArray(metadata.holder) ? metadata.holder : null;
3888
+
3889
+ // 编辑详情接口可能只返回 metadata.holder;UI 与提交链路统一读取 booking.holder。
3890
+ if (!next.holder && metadataHolder) {
3891
+ next.holder = {
3892
+ ...metadataHolder
3893
+ };
3894
+ }
3895
+ const holder = next.holder && typeof next.holder === 'object' && !Array.isArray(next.holder) ? next.holder : null;
3896
+ const holderRecords = Array.isArray(holder?.form_record) ? holder.form_record.filter(id => id !== undefined && id !== null && id !== '') : [];
3897
+ const hasMetadataHolderId = metadata.holder_id !== undefined && metadata.holder_id !== null && metadata.holder_id !== '' && (!Array.isArray(metadata.holder_id) || metadata.holder_id.length > 0);
3898
+
3899
+ // 卡片和编辑抽屉会从 holder_id 进入展示/表单回显,缺失时由 holder.form_record 补齐。
3900
+ if (!hasMetadataHolderId && holderRecords.length > 0) {
3901
+ metadata.holder_id = holderRecords;
3902
+ }
3903
+ if (Object.keys(metadata).length > 0) {
3904
+ next.metadata = metadata;
3905
+ }
3906
+ return next;
3907
+ }
3908
+
3909
+ /**
3910
+ * 匿名 booking 没有服务端行 ID 时,商品侧已有的 booking_uid 就是唯一可用的强引用。
3911
+ * 在统一规范化生成 UUID 前先复用它;已落库 booking 仍按 schedule_event_id 派生稳定 UID。
3912
+ */
3913
+ seedAnonymousBookingUidsFromProducts(raw) {
3914
+ if (!raw || typeof raw !== 'object') return {};
3915
+ if (!Array.isArray(raw.bookings) || !Array.isArray(raw.products)) return raw;
3916
+ const bookingUidsByProductUid = new Map();
3917
+ for (const product of raw.products) {
3918
+ const productUid = (0, _orderCollectionIdentity.getOrderCollectionUid)('product', product);
3919
+ const bookingUid = product?.booking_uid || product?.metadata?.booking_uid;
3920
+ if (!productUid || bookingUid === undefined || bookingUid === null || bookingUid === '') {
3921
+ continue;
3922
+ }
3923
+ const candidates = bookingUidsByProductUid.get(productUid) || new Set();
3924
+ candidates.add(String(bookingUid));
3925
+ bookingUidsByProductUid.set(productUid, candidates);
3926
+ }
3927
+ if (bookingUidsByProductUid.size === 0) return raw;
3928
+ const anonymousBookingCountByProductUid = new Map();
3929
+ for (const booking of raw.bookings) {
3930
+ if ((0, _orderCollectionIdentity.getOrderCollectionPersistentId)('booking', booking) || (0, _orderCollectionIdentity.getOrderCollectionUid)('booking', booking)) {
3931
+ continue;
3932
+ }
3933
+ const productUid = booking?.product_uid || booking?.metadata?.product_uid;
3934
+ if (productUid === undefined || productUid === null || productUid === '') continue;
3935
+ const key = String(productUid);
3936
+ anonymousBookingCountByProductUid.set(key, (anonymousBookingCountByProductUid.get(key) || 0) + 1);
3937
+ }
3938
+ return {
3939
+ ...raw,
3940
+ bookings: raw.bookings.map(booking => {
3941
+ if ((0, _orderCollectionIdentity.getOrderCollectionPersistentId)('booking', booking) || (0, _orderCollectionIdentity.getOrderCollectionUid)('booking', booking)) {
3942
+ return booking;
3943
+ }
3944
+ const productUid = booking?.product_uid || booking?.metadata?.product_uid;
3945
+ if (productUid === undefined || productUid === null || productUid === '') {
3946
+ return booking;
3947
+ }
3948
+ const productUidKey = String(productUid);
3949
+ // 多条匿名 booking 共用同一商品引用时身份仍然不明确,不能借商品字段猜测合并。
3950
+ if (anonymousBookingCountByProductUid.get(productUidKey) !== 1) {
3951
+ return booking;
3952
+ }
3953
+ const bookingUidCandidates = bookingUidsByProductUid.get(productUidKey);
3954
+ const bookingUid = bookingUidCandidates?.size === 1 ? [...bookingUidCandidates][0] : null;
3955
+ return bookingUid ? (0, _orderCollectionIdentity.setOrderCollectionUid)('booking', booking, bookingUid) : booking;
3956
+ })
3957
+ };
3958
+ }
3959
+ normalizeTempOrderForRuntime(raw, options) {
3960
+ const nextTempOrder = {
3961
+ ...this.createDefaultTempOrderInstance(),
3962
+ ...(raw || {})
3963
+ };
3964
+ // ES 列表详情使用 id 表示订单主键;OS 详情态统一依赖 order_id 驱动操作按钮。
3965
+ if ((nextTempOrder.order_id === undefined || nextTempOrder.order_id === null) && raw?.id !== undefined && raw?.id !== null) {
3966
+ nextTempOrder.order_id = raw.id;
3967
+ }
3968
+ delete nextTempOrder.summary;
3969
+ delete nextTempOrder.lastOrderInfo;
3970
+ delete nextTempOrder.discount_list;
3971
+ delete nextTempOrder.shop_id;
3972
+ delete nextTempOrder.create_date;
3973
+ delete nextTempOrder.total_amount;
3974
+ delete nextTempOrder.expect_amount;
3975
+ delete nextTempOrder.paid_amount;
3976
+ delete nextTempOrder.shop_note;
3977
+ delete nextTempOrder.start_time;
3978
+ delete nextTempOrder.tax_fee;
3979
+ delete nextTempOrder.total_refund_amount;
3980
+ delete nextTempOrder.holder_id;
3981
+ nextTempOrder.metadata = raw.metadata && typeof raw.metadata === 'object' ? {
3982
+ ...raw.metadata
3983
+ } : {};
3984
+ nextTempOrder.holder = raw.holder && typeof raw.holder === 'object' ? {
3985
+ ...raw.holder
3986
+ } : null;
3987
+ const rawProducts = Array.isArray(raw.products) ? raw.products : [];
3988
+ nextTempOrder.products = rawProducts.length > 0 ? rawProducts.map(p => this.normalizeHydratedOrderProduct(p, {
3989
+ makeEditMark: options?.makeEditMark
3990
+ })) : [];
3991
+ nextTempOrder.bookings = Array.isArray(raw.bookings) ? raw.bookings.map(b => {
3992
+ const booking = this.normalizeHydratedBookingHolder(b || {});
3993
+ return {
3994
+ ...booking,
3995
+ is_all: (0, _utils.normalizeBookingIsAll)(booking),
3996
+ sub_type: (0, _utils.normalizeBookingSubType)(booking)
3997
+ };
3998
+ }) : [];
3999
+ nextTempOrder.payments = Array.isArray(raw.payments) ? raw.payments.map(p => ({
4000
+ ...(p || {})
4001
+ })) : [];
4002
+ nextTempOrder.surcharges = Array.isArray(raw.surcharges) ? raw.surcharges.map(s => ({
4003
+ ...(s || {})
4004
+ })) : [];
4005
+ nextTempOrder.relation_forms = Array.isArray(raw.relation_forms) ? raw.relation_forms.map(f => ({
4006
+ ...(f || {})
4007
+ })) : [];
4008
+ nextTempOrder.contacts = Array.isArray(raw.contacts) ? raw.contacts.map(c => ({
4009
+ ...(c || {})
4010
+ })) : [];
4011
+ nextTempOrder.contacts_info = raw.contacts_info && typeof raw.contacts_info === 'object' && !Array.isArray(raw.contacts_info) ? {
4012
+ ...raw.contacts_info
4013
+ } : null;
4014
+ nextTempOrder._extend = raw._extend && typeof raw._extend === 'object' ? {
4015
+ ...raw._extend,
4016
+ productsByUid: raw._extend.productsByUid || {}
4017
+ } : {
4018
+ productsByUid: {}
4019
+ };
4020
+ const tempOrderExtend = nextTempOrder._extend;
4021
+ const productsByUid = tempOrderExtend.productsByUid || {};
4022
+ for (const [index, product] of nextTempOrder.products.entries()) {
4023
+ const uid = product.metadata?.unique_identification_number;
4024
+ if (!uid) continue;
4025
+ const existed = productsByUid[uid] && typeof productsByUid[uid] === 'object' ? productsByUid[uid] : {};
4026
+ const rawProduct = rawProducts[index] && typeof rawProducts[index] === 'object' ? rawProducts[index] : product;
4027
+ const origin = existed.origin ?? rawProduct._origin ?? rawProduct.metadata?.origin ?? rawProduct;
4028
+ const originSnapshot = (0, _lodashEs.cloneDeep)(origin);
4029
+ const productOptionString = this.buildHydratedProductOptionString(rawProduct) || this.buildHydratedProductOptionString(product);
4030
+ if (productOptionString && originSnapshot && typeof originSnapshot === 'object') {
4031
+ if (originSnapshot.product_option_string === undefined) {
4032
+ originSnapshot.product_option_string = productOptionString;
4033
+ }
4034
+ if (originSnapshot.product_options_stirng === undefined) {
4035
+ originSnapshot.product_options_stirng = productOptionString;
4036
+ }
4037
+ }
4038
+ productsByUid[uid] = {
4039
+ ...existed,
4040
+ origin: originSnapshot
4041
+ };
4042
+ }
4043
+ tempOrderExtend.productsByUid = productsByUid;
4044
+ this.hydrateLinkedBookingIdentity(nextTempOrder);
4045
+ if (options?.ensureIdentity !== false) {
4046
+ this.ensureExternalSaleNumber(nextTempOrder);
4047
+ }
4048
+ nextTempOrder.vouchers = raw.vouchers || [];
4049
+ return nextTempOrder;
4050
+ }
4051
+ hydrateLinkedBookingIdentity(tempOrder) {
4052
+ const bookingUidByProductUid = new Map();
4053
+ const productByUid = new Map();
4054
+ (tempOrder.products || []).forEach(product => {
4055
+ const productUid = product?.metadata?.unique_identification_number || product?.unique_identification_number;
4056
+ if (productUid !== undefined && productUid !== null && String(productUid) !== '') {
4057
+ productByUid.set(String(productUid), product);
4058
+ }
4059
+ });
4060
+ (tempOrder.bookings || []).forEach(booking => {
4061
+ if (!booking || typeof booking !== 'object') return;
4062
+ if (!booking.metadata || typeof booking.metadata !== 'object') {
4063
+ booking.metadata = {};
4064
+ }
4065
+ const productUid = booking.product_uid || booking.metadata.product_uid;
4066
+ if (productUid === undefined || productUid === null || String(productUid) === '') return;
4067
+ const matchedProduct = productByUid.get(String(productUid));
4068
+ const existedProductBookingUid = matchedProduct?.booking_uid || matchedProduct?.metadata?.booking_uid;
4069
+ // 客户端订单详情可能缺少 booking 自身 uid,这里补齐后才能反写到商品行。
4070
+ const bookingUid = String(booking.metadata.unique_identification_number || existedProductBookingUid || (0, _utils.createUuidV4)());
4071
+ booking.metadata.unique_identification_number = bookingUid;
4072
+ bookingUidByProductUid.set(String(productUid), bookingUid);
4073
+ });
4074
+ (tempOrder.products || []).forEach(product => {
4075
+ const productUid = product?.metadata?.unique_identification_number || product?.unique_identification_number;
4076
+ if (productUid === undefined || productUid === null || String(productUid) === '') return;
4077
+ const bookingUid = bookingUidByProductUid.get(String(productUid));
4078
+ if (!bookingUid) return;
4079
+ if (!product.metadata || typeof product.metadata !== 'object') {
4080
+ product.metadata = {};
4081
+ }
4082
+ // booking 已经过统一身份规范化;以它为准同步商品侧旧引用,避免两侧 UID 断链。
4083
+ const linkedBookingUid = String(bookingUid);
4084
+ product.booking_uid = linkedBookingUid;
4085
+ product.metadata.booking_uid = linkedBookingUid;
4086
+ });
4087
+ this.syncLinkedBookingHoldersToProducts(tempOrder);
4088
+ }
4089
+ pickHydratedDisplayText(value) {
4090
+ if (value === undefined || value === null) return '';
4091
+ if (typeof value === 'string') return value;
4092
+ if (typeof value !== 'object') return String(value);
4093
+ return value.auto || value.original || value.en || value['zh-CN'] || value['zh-HK'] || '';
4094
+ }
4095
+ formatHydratedNamePair(group, item) {
4096
+ const groupText = this.pickHydratedDisplayText(group);
4097
+ const itemText = this.pickHydratedDisplayText(item);
4098
+ if (groupText && itemText) return `${groupText}:${itemText}`;
4099
+ return groupText || itemText;
4100
+ }
4101
+ buildHydratedProductOptionString(product) {
4102
+ const segments = [];
4103
+ const sku = product?.product_sku || {};
4104
+ if (Array.isArray(sku.variant)) {
4105
+ for (const variant of sku.variant) {
4106
+ const segment = this.formatHydratedNamePair(variant?.group, variant?.item);
4107
+ if (segment) segments.push(segment);
4108
+ }
4109
+ }
4110
+ return segments.join(', ');
4111
+ }
4112
+ normalizeHydratedProductOptionItem(optionItem) {
4113
+ const rawNum = optionItem?.num ?? optionItem?.quantity ?? 1;
4114
+ const parsedNum = Number(rawNum);
4115
+ const rawPrice = optionItem?.add_price ?? optionItem?.price ?? 0;
4116
+ const parsedPrice = Number(rawPrice);
4117
+ const normalized = {
4118
+ ...optionItem,
4119
+ option_group_item_id: optionItem?.option_group_item_id,
4120
+ option_group_id: optionItem?.option_group_id,
4121
+ num: Number.isFinite(parsedNum) && parsedNum > 0 ? parsedNum : 1,
4122
+ add_price: Number.isFinite(parsedPrice) ? parsedPrice : 0
4123
+ };
4124
+ delete normalized.price;
4125
+ if (optionItem?.name !== undefined) normalized.name = optionItem.name;
4126
+ if (optionItem?.group_name !== undefined) normalized.group_name = optionItem.group_name;
4127
+ if (optionItem?.id !== undefined) normalized.id = optionItem.id;
4128
+ return normalized;
4129
+ }
4130
+ normalizeHydratedOrderProduct(product, options) {
4131
+ let row = {
4132
+ ...(product || {})
4133
+ };
4134
+ if (row.num === undefined && row.product_quantity !== undefined) {
4135
+ row.num = row.product_quantity;
4136
+ }
4137
+ const productSku = (0, _utils.ensureProductSku)(row);
4138
+ row.product_sku = {
4139
+ ...productSku,
4140
+ option: (0, _utils.normalizeProductSkuOptions)(productSku.option.map(optionItem => this.normalizeHydratedProductOptionItem(optionItem || {})))
4141
+ };
4142
+ delete row[`product_${'option'}_item`];
4143
+ if (!Array.isArray(row.product_bundle)) row.product_bundle = [];
4144
+ if (!row.metadata || typeof row.metadata !== 'object') row.metadata = {};
4145
+ if (row.booking_uid && !row.metadata.booking_uid) {
4146
+ row.metadata.booking_uid = row.booking_uid;
4147
+ }
4148
+ row = (0, _bundleDiscountHydration.restoreHydratedBundleDiscounts)(row, _utils.normalizeOrderProductDiscountList);
4149
+ const existingIdentity = row.metadata.unique_identification_number || row.unique_identification_number;
4150
+ if (!existingIdentity) {
4151
+ row.metadata.unique_identification_number = (0, _utils.createUuidV4)();
4152
+ }
4153
+ // 从云端、本地数据库中获取到的商品数据,先打上 runtime 标记,提供给 UI 做 saved items.
4154
+ if (options?.makeEditMark) {
4155
+ row.metadata.is_edit_for_runtime = true;
4156
+ }
4157
+ const normalized = (0, _utils3.normalizeOrderProduct)(row);
4158
+ normalized.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalized.discount_list);
4159
+ const result = {
4160
+ ...row,
4161
+ ...normalized,
4162
+ product_sku: normalized.product_sku,
4163
+ metadata: {
4164
+ ...row.metadata,
4165
+ ...normalized.metadata
4166
+ }
4167
+ };
4168
+ if (result.metadata) delete result.metadata.price_schema_version;
4169
+ const restored = (0, _bundleDiscountHydration.restoreHydratedBundleDiscounts)(result, _utils.normalizeOrderProductDiscountList);
4170
+ return this.sanitizeOrderProductForTempOrder(restored);
4171
+ }
850
4172
  getLastOrderInfo() {
851
- return this.store?.tempOrder?.lastOrderInfo;
4173
+ return this.store?.lastOrderInfo;
4174
+ }
4175
+ async getOrderInfo(order_id) {
4176
+ const res = await this.request.get(`/order/sales/${order_id}`, {
4177
+ with: ['products', 'bookings']
4178
+ }, {
4179
+ osServer: true
4180
+ });
4181
+ return res;
4182
+ }
4183
+ getVouchers() {
4184
+ return this.store.tempOrder?.vouchers || [];
852
4185
  }
853
4186
  }
854
4187
  exports.OrderModule = OrderModule;