@adtrackify/at-service-common 3.19.23 → 3.19.25

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 (747) hide show
  1. package/dist/cjs/__tests__/clients/acuity-client.spec.d.ts +1 -1
  2. package/dist/cjs/__tests__/clients/acuity-client.spec.js +43 -43
  3. package/dist/cjs/__tests__/clients/cross-platform-compression.spec.d.ts +1 -1
  4. package/dist/cjs/__tests__/clients/cross-platform-compression.spec.js +354 -354
  5. package/dist/cjs/__tests__/clients/dynamodb-client.spec.d.ts +1 -1
  6. package/dist/cjs/__tests__/clients/dynamodb-client.spec.js +194 -194
  7. package/dist/cjs/__tests__/clients/sqs-bundled-client.spec.d.ts +1 -1
  8. package/dist/cjs/__tests__/clients/sqs-bundled-client.spec.js +931 -931
  9. package/dist/cjs/__tests__/clients/sqs-bundling-contracts.spec.d.ts +1 -1
  10. package/dist/cjs/__tests__/clients/sqs-bundling-contracts.spec.js +563 -563
  11. package/dist/cjs/__tests__/clients/sqs-client.spec.d.ts +1 -1
  12. package/dist/cjs/__tests__/clients/sqs-client.spec.js +191 -191
  13. package/dist/cjs/__tests__/clients/sqs-unbundle.spec.d.ts +1 -1
  14. package/dist/cjs/__tests__/clients/sqs-unbundle.spec.js +1357 -1357
  15. package/dist/cjs/__tests__/db/contact-enrichments-db-service.spec.d.ts +1 -1
  16. package/dist/cjs/__tests__/db/contact-enrichments-db-service.spec.js +68 -68
  17. package/dist/cjs/__tests__/db/destinations-db-service.spec.d.ts +1 -1
  18. package/dist/cjs/__tests__/db/destinations-db-service.spec.js +125 -125
  19. package/dist/cjs/__tests__/db/products-db-service.spec.d.ts +1 -0
  20. package/dist/cjs/__tests__/db/products-db-service.spec.js +90 -0
  21. package/dist/cjs/__tests__/db/products-db-service.spec.js.map +1 -0
  22. package/dist/cjs/__tests__/db/shared-read-db-services.spec.d.ts +1 -1
  23. package/dist/cjs/__tests__/db/shared-read-db-services.spec.js +89 -89
  24. package/dist/cjs/__tests__/db/shopify-app-installs-db-service.spec.d.ts +1 -1
  25. package/dist/cjs/__tests__/db/shopify-app-installs-db-service.spec.js +104 -104
  26. package/dist/cjs/__tests__/db/subscriptions-db-service.spec.d.ts +1 -1
  27. package/dist/cjs/__tests__/db/subscriptions-db-service.spec.js +95 -95
  28. package/dist/cjs/__tests__/db/user-accounts-db-service.spec.d.ts +1 -1
  29. package/dist/cjs/__tests__/db/user-accounts-db-service.spec.js +76 -76
  30. package/dist/cjs/__tests__/helpers/account-users-helper.spec.d.ts +1 -1
  31. package/dist/cjs/__tests__/helpers/account-users-helper.spec.js +220 -220
  32. package/dist/cjs/__tests__/helpers/acuity-helper.spec.d.ts +1 -1
  33. package/dist/cjs/__tests__/helpers/acuity-helper.spec.js +69 -69
  34. package/dist/cjs/__tests__/helpers/api-key-auth-helper.spec.d.ts +1 -1
  35. package/dist/cjs/__tests__/helpers/api-key-auth-helper.spec.js +82 -82
  36. package/dist/cjs/__tests__/identity-cache/identity-cache-db-service.spec.d.ts +1 -1
  37. package/dist/cjs/__tests__/identity-cache/identity-cache-db-service.spec.js +674 -674
  38. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +1 -1
  39. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +1140 -1140
  40. package/dist/cjs/__tests__/identity-cache/trait-merging-and-staleness.spec.d.ts +1 -1
  41. package/dist/cjs/__tests__/identity-cache/trait-merging-and-staleness.spec.js +588 -588
  42. package/dist/cjs/__tests__/integration/sqs-bundling-roundtrip.spec.d.ts +1 -1
  43. package/dist/cjs/__tests__/integration/sqs-bundling-roundtrip.spec.js +584 -584
  44. package/dist/cjs/__tests__/libs/compress-decompress.spec.d.ts +1 -1
  45. package/dist/cjs/__tests__/libs/compress-decompress.spec.js +16 -16
  46. package/dist/cjs/__tests__/libs/contacts.spec.d.ts +1 -1
  47. package/dist/cjs/__tests__/libs/contacts.spec.js +294 -294
  48. package/dist/cjs/__tests__/libs/currency.spec.d.ts +1 -1
  49. package/dist/cjs/__tests__/libs/currency.spec.js +220 -220
  50. package/dist/cjs/__tests__/libs/dates.spec.d.ts +1 -1
  51. package/dist/cjs/__tests__/libs/dates.spec.js +130 -130
  52. package/dist/cjs/__tests__/libs/domain.spec.d.ts +1 -1
  53. package/dist/cjs/__tests__/libs/domain.spec.js +107 -107
  54. package/dist/cjs/__tests__/libs/numbers.spec.d.ts +1 -1
  55. package/dist/cjs/__tests__/libs/numbers.spec.js +261 -261
  56. package/dist/cjs/__tests__/s3-client/s3-client.spec.d.ts +1 -1
  57. package/dist/cjs/__tests__/s3-client/s3-client.spec.js +33 -33
  58. package/dist/cjs/__tests__/services/acuity-api-service.spec.d.ts +1 -1
  59. package/dist/cjs/__tests__/services/acuity-api-service.spec.js +71 -71
  60. package/dist/cjs/__tests__/services/cost/cost-calculation-types.spec.d.ts +1 -0
  61. package/dist/cjs/__tests__/services/cost/cost-calculation-types.spec.js +24 -0
  62. package/dist/cjs/__tests__/services/cost/cost-calculation-types.spec.js.map +1 -0
  63. package/dist/cjs/__tests__/services/cost/cost-calculator-service.spec.d.ts +1 -0
  64. package/dist/cjs/__tests__/services/cost/cost-calculator-service.spec.js +3320 -0
  65. package/dist/cjs/__tests__/services/cost/cost-calculator-service.spec.js.map +1 -0
  66. package/dist/cjs/__tests__/services/cost/cost-currency-service.spec.d.ts +1 -0
  67. package/dist/cjs/__tests__/services/cost/cost-currency-service.spec.js +115 -0
  68. package/dist/cjs/__tests__/services/cost/cost-currency-service.spec.js.map +1 -0
  69. package/dist/cjs/__tests__/services/cost/cost-filter-service.spec.d.ts +1 -0
  70. package/dist/cjs/__tests__/services/cost/cost-filter-service.spec.js +469 -0
  71. package/dist/cjs/__tests__/services/cost/cost-filter-service.spec.js.map +1 -0
  72. package/dist/cjs/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.d.ts +1 -0
  73. package/dist/cjs/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js +207 -0
  74. package/dist/cjs/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js.map +1 -0
  75. package/dist/cjs/__tests__/services/currency-exchange-rate-lookup-service.spec.d.ts +1 -0
  76. package/dist/cjs/__tests__/services/currency-exchange-rate-lookup-service.spec.js +35 -0
  77. package/dist/cjs/__tests__/services/currency-exchange-rate-lookup-service.spec.js.map +1 -0
  78. package/dist/cjs/__tests__/services/email-verification/contact-email-verification-service.spec.d.ts +1 -1
  79. package/dist/cjs/__tests__/services/email-verification/contact-email-verification-service.spec.js +93 -93
  80. package/dist/cjs/__tests__/services/email-verification/email-verification-service.spec.d.ts +1 -1
  81. package/dist/cjs/__tests__/services/email-verification/email-verification-service.spec.js +57 -57
  82. package/dist/cjs/__tests__/shopify/shopify-graphql-transformer.spec.d.ts +1 -1
  83. package/dist/cjs/__tests__/shopify/shopify-graphql-transformer.spec.js +35 -35
  84. package/dist/cjs/__tests__/unit/libs/api-router/public-api-router.spec.d.ts +1 -1
  85. package/dist/cjs/__tests__/unit/libs/api-router/public-api-router.spec.js +181 -181
  86. package/dist/cjs/__tests__/unit/libs/api-router/route-matcher.spec.d.ts +1 -1
  87. package/dist/cjs/__tests__/unit/libs/api-router/route-matcher.spec.js +69 -69
  88. package/dist/cjs/__tests__/utils/custom-measure-formula-utils.spec.d.ts +1 -1
  89. package/dist/cjs/__tests__/utils/custom-measure-formula-utils.spec.js +139 -139
  90. package/dist/cjs/clients/generic/cognito-client.d.ts +23 -23
  91. package/dist/cjs/clients/generic/cognito-client.js +209 -209
  92. package/dist/cjs/clients/generic/dynamodb-client.d.ts +20 -20
  93. package/dist/cjs/clients/generic/dynamodb-client.js +235 -235
  94. package/dist/cjs/clients/generic/eventbridge-client.d.ts +14 -14
  95. package/dist/cjs/clients/generic/eventbridge-client.js +51 -51
  96. package/dist/cjs/clients/generic/http-client.d.ts +14 -14
  97. package/dist/cjs/clients/generic/http-client.js +61 -61
  98. package/dist/cjs/clients/generic/index.d.ts +13 -13
  99. package/dist/cjs/clients/generic/index.js +29 -29
  100. package/dist/cjs/clients/generic/lambda-invoke-client.d.ts +10 -10
  101. package/dist/cjs/clients/generic/lambda-invoke-client.js +39 -39
  102. package/dist/cjs/clients/generic/location-client.d.ts +8 -8
  103. package/dist/cjs/clients/generic/location-client.js +31 -31
  104. package/dist/cjs/clients/generic/redis-client.d.ts +33 -33
  105. package/dist/cjs/clients/generic/redis-client.js +191 -191
  106. package/dist/cjs/clients/generic/s3-client.d.ts +23 -23
  107. package/dist/cjs/clients/generic/s3-client.js +216 -216
  108. package/dist/cjs/clients/generic/singlestore-db-client.d.ts +14 -14
  109. package/dist/cjs/clients/generic/singlestore-db-client.js +67 -67
  110. package/dist/cjs/clients/generic/sqs-bundled-client.d.ts +15 -15
  111. package/dist/cjs/clients/generic/sqs-bundled-client.js +311 -311
  112. package/dist/cjs/clients/generic/sqs-bundled-client.types.d.ts +53 -53
  113. package/dist/cjs/clients/generic/sqs-bundled-client.types.js +17 -17
  114. package/dist/cjs/clients/generic/sqs-client.d.ts +53 -53
  115. package/dist/cjs/clients/generic/sqs-client.js +285 -285
  116. package/dist/cjs/clients/generic/sqs-unbundle.d.ts +32 -32
  117. package/dist/cjs/clients/generic/sqs-unbundle.js +144 -144
  118. package/dist/cjs/clients/index.d.ts +3 -3
  119. package/dist/cjs/clients/index.js +19 -19
  120. package/dist/cjs/clients/internal-api/accounts-client.d.ts +91 -91
  121. package/dist/cjs/clients/internal-api/accounts-client.js +129 -129
  122. package/dist/cjs/clients/internal-api/cache-lambda-client.d.ts +26 -26
  123. package/dist/cjs/clients/internal-api/cache-lambda-client.js +89 -89
  124. package/dist/cjs/clients/internal-api/db-management-client.d.ts +18 -18
  125. package/dist/cjs/clients/internal-api/db-management-client.js +36 -36
  126. package/dist/cjs/clients/internal-api/destinations-client.d.ts +34 -34
  127. package/dist/cjs/clients/internal-api/destinations-client.js +79 -79
  128. package/dist/cjs/clients/internal-api/event-collector-client.d.ts +20 -20
  129. package/dist/cjs/clients/internal-api/event-collector-client.js +36 -36
  130. package/dist/cjs/clients/internal-api/identity-client.d.ts +31 -31
  131. package/dist/cjs/clients/internal-api/identity-client.js +91 -91
  132. package/dist/cjs/clients/internal-api/index.d.ts +9 -9
  133. package/dist/cjs/clients/internal-api/index.js +25 -25
  134. package/dist/cjs/clients/internal-api/shopify-app-install-client.d.ts +37 -37
  135. package/dist/cjs/clients/internal-api/shopify-app-install-client.js +81 -81
  136. package/dist/cjs/clients/internal-api/subscriptions-client.d.ts +26 -26
  137. package/dist/cjs/clients/internal-api/subscriptions-client.js +77 -77
  138. package/dist/cjs/clients/internal-api/users-auth-client.d.ts +35 -35
  139. package/dist/cjs/clients/internal-api/users-auth-client.js +110 -110
  140. package/dist/cjs/clients/third-party/acuity-client.d.ts +10 -10
  141. package/dist/cjs/clients/third-party/acuity-client.js +40 -40
  142. package/dist/cjs/clients/third-party/emailable-client.d.ts +7 -7
  143. package/dist/cjs/clients/third-party/emailable-client.js +25 -25
  144. package/dist/cjs/clients/third-party/exchange-rate-api-client.d.ts +17 -17
  145. package/dist/cjs/clients/third-party/exchange-rate-api-client.js +19 -19
  146. package/dist/cjs/clients/third-party/index.d.ts +5 -5
  147. package/dist/cjs/clients/third-party/index.js +21 -21
  148. package/dist/cjs/clients/third-party/loops-client.d.ts +10 -10
  149. package/dist/cjs/clients/third-party/loops-client.js +30 -30
  150. package/dist/cjs/clients/third-party/shopify/graphql-order-queries.d.ts +25 -25
  151. package/dist/cjs/clients/third-party/shopify/graphql-order-queries.js +4 -4
  152. package/dist/cjs/clients/third-party/shopify/graphql-product-queries.d.ts +2 -2
  153. package/dist/cjs/clients/third-party/shopify/graphql-product-queries.js +5 -5
  154. package/dist/cjs/clients/third-party/shopify/shopify-graphql-client.d.ts +10 -10
  155. package/dist/cjs/clients/third-party/shopify/shopify-graphql-client.js +161 -161
  156. package/dist/cjs/clients/third-party/shopify-client.d.ts +29 -29
  157. package/dist/cjs/clients/third-party/shopify-client.js +146 -146
  158. package/dist/cjs/constants/index.d.ts +1 -1
  159. package/dist/cjs/constants/index.js +17 -17
  160. package/dist/cjs/constants/sqs.d.ts +20 -20
  161. package/dist/cjs/constants/sqs.js +26 -26
  162. package/dist/cjs/helpers/account-users-helper.d.ts +2 -2
  163. package/dist/cjs/helpers/account-users-helper.js +22 -22
  164. package/dist/cjs/helpers/acuity-helper.d.ts +4 -4
  165. package/dist/cjs/helpers/acuity-helper.js +56 -56
  166. package/dist/cjs/helpers/api-key-auth-helper.d.ts +9 -9
  167. package/dist/cjs/helpers/api-key-auth-helper.js +40 -40
  168. package/dist/cjs/helpers/api-key-authorizer-helper.d.ts +36 -36
  169. package/dist/cjs/helpers/api-key-authorizer-helper.js +87 -87
  170. package/dist/cjs/helpers/identity-cache-helper.d.ts +21 -21
  171. package/dist/cjs/helpers/identity-cache-helper.js +157 -157
  172. package/dist/cjs/helpers/index.d.ts +10 -10
  173. package/dist/cjs/helpers/index.js +26 -26
  174. package/dist/cjs/helpers/input-validation-helper.d.ts +3 -3
  175. package/dist/cjs/helpers/input-validation-helper.js +22 -22
  176. package/dist/cjs/helpers/logging-helper.d.ts +16 -16
  177. package/dist/cjs/helpers/logging-helper.js +84 -84
  178. package/dist/cjs/helpers/response-helper.d.ts +18 -18
  179. package/dist/cjs/helpers/response-helper.js +43 -43
  180. package/dist/cjs/helpers/shopify-helper.d.ts +9 -9
  181. package/dist/cjs/helpers/shopify-helper.js +26 -26
  182. package/dist/cjs/helpers/sqs-utils.d.ts +6 -6
  183. package/dist/cjs/helpers/sqs-utils.js +14 -14
  184. package/dist/cjs/index.d.ts +7 -7
  185. package/dist/cjs/index.js +23 -23
  186. package/dist/cjs/libs/api-router/index.d.ts +2 -2
  187. package/dist/cjs/libs/api-router/index.js +18 -18
  188. package/dist/cjs/libs/api-router/public-api-router.d.ts +3 -3
  189. package/dist/cjs/libs/api-router/public-api-router.js +36 -36
  190. package/dist/cjs/libs/api-router/route-matcher.d.ts +21 -21
  191. package/dist/cjs/libs/api-router/route-matcher.js +36 -36
  192. package/dist/cjs/libs/click-id-parser.d.ts +23 -23
  193. package/dist/cjs/libs/click-id-parser.js +49 -49
  194. package/dist/cjs/libs/compression.d.ts +2 -2
  195. package/dist/cjs/libs/compression.js +33 -33
  196. package/dist/cjs/libs/contacts.d.ts +7 -7
  197. package/dist/cjs/libs/contacts.js +152 -152
  198. package/dist/cjs/libs/cookie.d.ts +17 -17
  199. package/dist/cjs/libs/cookie.js +76 -76
  200. package/dist/cjs/libs/crypto.d.ts +4 -4
  201. package/dist/cjs/libs/crypto.js +25 -25
  202. package/dist/cjs/libs/csv.d.ts +2 -2
  203. package/dist/cjs/libs/csv.js +35 -35
  204. package/dist/cjs/libs/currency.d.ts +1 -1
  205. package/dist/cjs/libs/currency.js +29 -29
  206. package/dist/cjs/libs/dates.d.ts +12 -12
  207. package/dist/cjs/libs/dates.js +96 -96
  208. package/dist/cjs/libs/domain.d.ts +2 -2
  209. package/dist/cjs/libs/domain.js +38 -38
  210. package/dist/cjs/libs/emails.d.ts +8 -8
  211. package/dist/cjs/libs/emails.js +154 -154
  212. package/dist/cjs/libs/http-error.d.ts +21 -21
  213. package/dist/cjs/libs/http-error.js +63 -63
  214. package/dist/cjs/libs/http-status-codes.d.ts +58 -58
  215. package/dist/cjs/libs/http-status-codes.js +62 -62
  216. package/dist/cjs/libs/index.d.ts +19 -19
  217. package/dist/cjs/libs/index.js +35 -35
  218. package/dist/cjs/libs/numbers.d.ts +1 -1
  219. package/dist/cjs/libs/numbers.js +15 -15
  220. package/dist/cjs/libs/referrer-parser/index.d.ts +2 -2
  221. package/dist/cjs/libs/referrer-parser/index.js +18 -18
  222. package/dist/cjs/libs/referrer-parser/referrer-data.d.ts +9 -9
  223. package/dist/cjs/libs/referrer-parser/referrer-data.js +3307 -3307
  224. package/dist/cjs/libs/referrer-parser/referrer-parser-util.d.ts +20 -20
  225. package/dist/cjs/libs/referrer-parser/referrer-parser-util.js +131 -131
  226. package/dist/cjs/libs/strings.d.ts +3 -3
  227. package/dist/cjs/libs/strings.js +46 -46
  228. package/dist/cjs/libs/traits.d.ts +6 -6
  229. package/dist/cjs/libs/traits.js +65 -65
  230. package/dist/cjs/libs/url.d.ts +1 -1
  231. package/dist/cjs/libs/url.js +13 -13
  232. package/dist/cjs/services/acuity-api-service.d.ts +9 -9
  233. package/dist/cjs/services/acuity-api-service.js +73 -73
  234. package/dist/cjs/services/cache/generic-cached-object.d.ts +5 -5
  235. package/dist/cjs/services/cache/generic-cached-object.js +2 -2
  236. package/dist/cjs/services/cache/index.d.ts +1 -1
  237. package/dist/cjs/services/cache/index.js +17 -17
  238. package/dist/cjs/services/cache/product-cache-service.d.ts +21 -21
  239. package/dist/cjs/services/cache/product-cache-service.js +76 -76
  240. package/dist/cjs/services/cost/cost-calculation-types.d.ts +69 -0
  241. package/dist/cjs/services/cost/cost-calculation-types.js +20 -0
  242. package/dist/cjs/services/cost/cost-calculation-types.js.map +1 -0
  243. package/dist/cjs/services/cost/cost-calculator-service.d.ts +24 -0
  244. package/dist/cjs/services/cost/cost-calculator-service.js +457 -0
  245. package/dist/cjs/services/cost/cost-calculator-service.js.map +1 -0
  246. package/dist/cjs/services/cost/cost-currency-service.d.ts +6 -0
  247. package/dist/cjs/services/cost/cost-currency-service.js +88 -0
  248. package/dist/cjs/services/cost/cost-currency-service.js.map +1 -0
  249. package/dist/cjs/services/cost/cost-filter-service.d.ts +10 -0
  250. package/dist/cjs/services/cost/cost-filter-service.js +122 -0
  251. package/dist/cjs/services/cost/cost-filter-service.js.map +1 -0
  252. package/dist/cjs/services/cost/index.d.ts +5 -0
  253. package/dist/cjs/services/cost/index.js +22 -0
  254. package/dist/cjs/services/cost/index.js.map +1 -0
  255. package/dist/cjs/services/cost/order-cost/index.d.ts +2 -0
  256. package/dist/cjs/services/cost/order-cost/index.js +19 -0
  257. package/dist/cjs/services/cost/order-cost/index.js.map +1 -0
  258. package/dist/cjs/services/cost/order-cost/order-cost-resolution-service.d.ts +23 -0
  259. package/dist/cjs/services/cost/order-cost/order-cost-resolution-service.js +362 -0
  260. package/dist/cjs/services/cost/order-cost/order-cost-resolution-service.js.map +1 -0
  261. package/dist/cjs/services/cost/order-cost/order-cost-resolution-types.d.ts +37 -0
  262. package/dist/cjs/services/cost/order-cost/order-cost-resolution-types.js +3 -0
  263. package/dist/cjs/services/cost/order-cost/order-cost-resolution-types.js.map +1 -0
  264. package/dist/cjs/services/currency-exchange-rate-lookup-service.d.ts +12 -11
  265. package/dist/cjs/services/currency-exchange-rate-lookup-service.js +94 -66
  266. package/dist/cjs/services/currency-exchange-rate-lookup-service.js.map +1 -1
  267. package/dist/cjs/services/db/accounts-db-service.d.ts +9 -9
  268. package/dist/cjs/services/db/accounts-db-service.js +33 -33
  269. package/dist/cjs/services/db/api-keys-db-service.d.ts +10 -10
  270. package/dist/cjs/services/db/api-keys-db-service.js +36 -36
  271. package/dist/cjs/services/db/contact-enrichments-db-service.d.ts +15 -15
  272. package/dist/cjs/services/db/contact-enrichments-db-service.js +94 -94
  273. package/dist/cjs/services/db/currency-exchange-rates-db-service.d.ts +21 -21
  274. package/dist/cjs/services/db/currency-exchange-rates-db-service.js +39 -39
  275. package/dist/cjs/services/db/custom-measures-db-service.d.ts +14 -14
  276. package/dist/cjs/services/db/custom-measures-db-service.js +48 -48
  277. package/dist/cjs/services/db/destinations-db-service.d.ts +13 -13
  278. package/dist/cjs/services/db/destinations-db-service.js +74 -74
  279. package/dist/cjs/services/db/identity-cache-db-service.d.ts +28 -28
  280. package/dist/cjs/services/db/identity-cache-db-service.js +320 -320
  281. package/dist/cjs/services/db/identity-cache-dynamodb-service.d.ts +38 -34
  282. package/dist/cjs/services/db/identity-cache-dynamodb-service.js +439 -433
  283. package/dist/cjs/services/db/identity-cache-dynamodb-service.js.map +1 -1
  284. package/dist/cjs/services/db/index.d.ts +19 -17
  285. package/dist/cjs/services/db/index.js +35 -33
  286. package/dist/cjs/services/db/index.js.map +1 -1
  287. package/dist/cjs/services/db/log-events-db-service.d.ts +11 -11
  288. package/dist/cjs/services/db/log-events-db-service.js +181 -181
  289. package/dist/cjs/services/db/pixels-db-service.d.ts +8 -8
  290. package/dist/cjs/services/db/pixels-db-service.js +35 -35
  291. package/dist/cjs/services/db/products-db-service-types.d.ts +10 -0
  292. package/dist/cjs/services/db/products-db-service-types.js +3 -0
  293. package/dist/cjs/services/db/products-db-service-types.js.map +1 -0
  294. package/dist/cjs/services/db/products-db-service.d.ts +19 -0
  295. package/dist/cjs/services/db/products-db-service.js +282 -0
  296. package/dist/cjs/services/db/products-db-service.js.map +1 -0
  297. package/dist/cjs/services/db/purchasable-contacts-db-service.d.ts +9 -9
  298. package/dist/cjs/services/db/purchasable-contacts-db-service.js +43 -43
  299. package/dist/cjs/services/db/purchased-contacts/index.d.ts +2 -2
  300. package/dist/cjs/services/db/purchased-contacts/index.js +18 -18
  301. package/dist/cjs/services/db/purchased-contacts/purchased-contacts-db-service.d.ts +18 -18
  302. package/dist/cjs/services/db/purchased-contacts/purchased-contacts-db-service.js +152 -152
  303. package/dist/cjs/services/db/purchased-contacts/types.d.ts +11 -11
  304. package/dist/cjs/services/db/purchased-contacts/types.js +2 -2
  305. package/dist/cjs/services/db/shopify-app-installs-db-service.d.ts +10 -10
  306. package/dist/cjs/services/db/shopify-app-installs-db-service.js +52 -52
  307. package/dist/cjs/services/db/shopify-products-cache-db-service.d.ts +16 -16
  308. package/dist/cjs/services/db/shopify-products-cache-db-service.js +73 -73
  309. package/dist/cjs/services/db/subscriptions-db-service.d.ts +11 -11
  310. package/dist/cjs/services/db/subscriptions-db-service.js +38 -38
  311. package/dist/cjs/services/db/tracking-events-db-service.d.ts +21 -21
  312. package/dist/cjs/services/db/tracking-events-db-service.js +188 -188
  313. package/dist/cjs/services/db/user-accounts-db-service.d.ts +7 -7
  314. package/dist/cjs/services/db/user-accounts-db-service.js +17 -17
  315. package/dist/cjs/services/email-verification/contact-email-verification-service.d.ts +7 -7
  316. package/dist/cjs/services/email-verification/contact-email-verification-service.js +101 -101
  317. package/dist/cjs/services/email-verification/email-verification-service.d.ts +19 -19
  318. package/dist/cjs/services/email-verification/email-verification-service.js +131 -131
  319. package/dist/cjs/services/email-verification/index.d.ts +2 -2
  320. package/dist/cjs/services/email-verification/index.js +18 -18
  321. package/dist/cjs/services/eventbridge-integration-service.d.ts +9 -9
  322. package/dist/cjs/services/eventbridge-integration-service.js +28 -28
  323. package/dist/cjs/services/events/index.d.ts +3 -3
  324. package/dist/cjs/services/events/index.js +19 -19
  325. package/dist/cjs/services/events/log-event-service.d.ts +19 -19
  326. package/dist/cjs/services/events/log-event-service.js +77 -77
  327. package/dist/cjs/services/events/metric-event-service.d.ts +9 -9
  328. package/dist/cjs/services/events/metric-event-service.js +49 -49
  329. package/dist/cjs/services/events/tracking-event-sqs-service.d.ts +8 -8
  330. package/dist/cjs/services/events/tracking-event-sqs-service.js +34 -34
  331. package/dist/cjs/services/generic-cache-service.d.ts +7 -7
  332. package/dist/cjs/services/generic-cache-service.js +33 -33
  333. package/dist/cjs/services/index.d.ts +11 -10
  334. package/dist/cjs/services/index.js +27 -26
  335. package/dist/cjs/services/index.js.map +1 -1
  336. package/dist/cjs/services/ipdata-lookup-service.d.ts +20 -20
  337. package/dist/cjs/services/ipdata-lookup-service.js +112 -112
  338. package/dist/cjs/services/shopify/index.d.ts +2 -2
  339. package/dist/cjs/services/shopify/index.js +18 -18
  340. package/dist/cjs/services/shopify/products/index.d.ts +1 -1
  341. package/dist/cjs/services/shopify/products/index.js +17 -17
  342. package/dist/cjs/services/shopify/products/shopify-products-serviceV2.d.ts +17 -17
  343. package/dist/cjs/services/shopify/products/shopify-products-serviceV2.js +112 -112
  344. package/dist/cjs/services/shopify/shopify-graphql-transformer.d.ts +8 -8
  345. package/dist/cjs/services/shopify/shopify-graphql-transformer.js +141 -141
  346. package/dist/cjs/types/acuity-types.d.ts +74 -74
  347. package/dist/cjs/types/acuity-types.js +2 -2
  348. package/dist/cjs/types/api-response.d.ts +6 -6
  349. package/dist/cjs/types/api-response.js +2 -2
  350. package/dist/cjs/types/index.d.ts +4 -4
  351. package/dist/cjs/types/index.js +33 -33
  352. package/dist/cjs/types/internal-events/event-detail-types.d.ts +20 -20
  353. package/dist/cjs/types/internal-events/event-detail-types.js +27 -27
  354. package/dist/cjs/types/internal-events/index.d.ts +1 -1
  355. package/dist/cjs/types/internal-events/index.js +17 -17
  356. package/dist/cjs/types/shopify-graphql-types/admin.generated.d.ts +123 -123
  357. package/dist/cjs/types/shopify-graphql-types/admin.generated.js +2 -2
  358. package/dist/cjs/types/shopify-graphql-types/admin.types.d.ts +26289 -26289
  359. package/dist/cjs/types/shopify-graphql-types/admin.types.js +5311 -5311
  360. package/dist/cjs/types/shopify-graphql-types/index.d.ts +2 -2
  361. package/dist/cjs/types/shopify-graphql-types/index.js +18 -18
  362. package/dist/cjs/types/shopify-rest-types.d.ts +767 -767
  363. package/dist/cjs/types/shopify-rest-types.js +2 -2
  364. package/dist/cjs/utils/compression.d.ts +36 -36
  365. package/dist/cjs/utils/compression.js +198 -198
  366. package/dist/cjs/utils/custom-measure-formula-utils.d.ts +6 -6
  367. package/dist/cjs/utils/custom-measure-formula-utils.js +209 -209
  368. package/dist/cjs/utils/index.d.ts +4 -4
  369. package/dist/cjs/utils/index.js +20 -20
  370. package/dist/cjs/utils/retry-envelope.d.ts +12 -12
  371. package/dist/cjs/utils/retry-envelope.js +28 -28
  372. package/dist/cjs/utils/size.d.ts +2 -2
  373. package/dist/cjs/utils/size.js +49 -49
  374. package/dist/esm/__tests__/clients/acuity-client.spec.d.ts +1 -1
  375. package/dist/esm/__tests__/clients/acuity-client.spec.js +41 -41
  376. package/dist/esm/__tests__/clients/cross-platform-compression.spec.d.ts +1 -1
  377. package/dist/esm/__tests__/clients/cross-platform-compression.spec.js +329 -329
  378. package/dist/esm/__tests__/clients/dynamodb-client.spec.d.ts +1 -1
  379. package/dist/esm/__tests__/clients/dynamodb-client.spec.js +192 -192
  380. package/dist/esm/__tests__/clients/sqs-bundled-client.spec.d.ts +1 -1
  381. package/dist/esm/__tests__/clients/sqs-bundled-client.spec.js +906 -906
  382. package/dist/esm/__tests__/clients/sqs-bundling-contracts.spec.d.ts +1 -1
  383. package/dist/esm/__tests__/clients/sqs-bundling-contracts.spec.js +538 -538
  384. package/dist/esm/__tests__/clients/sqs-client.spec.d.ts +1 -1
  385. package/dist/esm/__tests__/clients/sqs-client.spec.js +189 -189
  386. package/dist/esm/__tests__/clients/sqs-unbundle.spec.d.ts +1 -1
  387. package/dist/esm/__tests__/clients/sqs-unbundle.spec.js +1355 -1355
  388. package/dist/esm/__tests__/db/contact-enrichments-db-service.spec.d.ts +1 -1
  389. package/dist/esm/__tests__/db/contact-enrichments-db-service.spec.js +66 -66
  390. package/dist/esm/__tests__/db/destinations-db-service.spec.d.ts +1 -1
  391. package/dist/esm/__tests__/db/destinations-db-service.spec.js +123 -123
  392. package/dist/esm/__tests__/db/products-db-service.spec.d.ts +1 -0
  393. package/dist/esm/__tests__/db/products-db-service.spec.js +88 -0
  394. package/dist/esm/__tests__/db/products-db-service.spec.js.map +1 -0
  395. package/dist/esm/__tests__/db/shared-read-db-services.spec.d.ts +1 -1
  396. package/dist/esm/__tests__/db/shared-read-db-services.spec.js +87 -87
  397. package/dist/esm/__tests__/db/shopify-app-installs-db-service.spec.d.ts +1 -1
  398. package/dist/esm/__tests__/db/shopify-app-installs-db-service.spec.js +102 -102
  399. package/dist/esm/__tests__/db/subscriptions-db-service.spec.d.ts +1 -1
  400. package/dist/esm/__tests__/db/subscriptions-db-service.spec.js +93 -93
  401. package/dist/esm/__tests__/db/user-accounts-db-service.spec.d.ts +1 -1
  402. package/dist/esm/__tests__/db/user-accounts-db-service.spec.js +74 -74
  403. package/dist/esm/__tests__/helpers/account-users-helper.spec.d.ts +1 -1
  404. package/dist/esm/__tests__/helpers/account-users-helper.spec.js +218 -218
  405. package/dist/esm/__tests__/helpers/acuity-helper.spec.d.ts +1 -1
  406. package/dist/esm/__tests__/helpers/acuity-helper.spec.js +67 -67
  407. package/dist/esm/__tests__/helpers/api-key-auth-helper.spec.d.ts +1 -1
  408. package/dist/esm/__tests__/helpers/api-key-auth-helper.spec.js +80 -80
  409. package/dist/esm/__tests__/identity-cache/identity-cache-db-service.spec.d.ts +1 -1
  410. package/dist/esm/__tests__/identity-cache/identity-cache-db-service.spec.js +672 -672
  411. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +1 -1
  412. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +1138 -1138
  413. package/dist/esm/__tests__/identity-cache/trait-merging-and-staleness.spec.d.ts +1 -1
  414. package/dist/esm/__tests__/identity-cache/trait-merging-and-staleness.spec.js +586 -586
  415. package/dist/esm/__tests__/integration/sqs-bundling-roundtrip.spec.d.ts +1 -1
  416. package/dist/esm/__tests__/integration/sqs-bundling-roundtrip.spec.js +582 -582
  417. package/dist/esm/__tests__/libs/compress-decompress.spec.d.ts +1 -1
  418. package/dist/esm/__tests__/libs/compress-decompress.spec.js +14 -14
  419. package/dist/esm/__tests__/libs/contacts.spec.d.ts +1 -1
  420. package/dist/esm/__tests__/libs/contacts.spec.js +292 -292
  421. package/dist/esm/__tests__/libs/currency.spec.d.ts +1 -1
  422. package/dist/esm/__tests__/libs/currency.spec.js +218 -218
  423. package/dist/esm/__tests__/libs/dates.spec.d.ts +1 -1
  424. package/dist/esm/__tests__/libs/dates.spec.js +128 -128
  425. package/dist/esm/__tests__/libs/domain.spec.d.ts +1 -1
  426. package/dist/esm/__tests__/libs/domain.spec.js +105 -105
  427. package/dist/esm/__tests__/libs/numbers.spec.d.ts +1 -1
  428. package/dist/esm/__tests__/libs/numbers.spec.js +259 -259
  429. package/dist/esm/__tests__/s3-client/s3-client.spec.d.ts +1 -1
  430. package/dist/esm/__tests__/s3-client/s3-client.spec.js +31 -31
  431. package/dist/esm/__tests__/services/acuity-api-service.spec.d.ts +1 -1
  432. package/dist/esm/__tests__/services/acuity-api-service.spec.js +69 -69
  433. package/dist/esm/__tests__/services/cost/cost-calculation-types.spec.d.ts +1 -0
  434. package/dist/esm/__tests__/services/cost/cost-calculation-types.spec.js +22 -0
  435. package/dist/esm/__tests__/services/cost/cost-calculation-types.spec.js.map +1 -0
  436. package/dist/esm/__tests__/services/cost/cost-calculator-service.spec.d.ts +1 -0
  437. package/dist/esm/__tests__/services/cost/cost-calculator-service.spec.js +3318 -0
  438. package/dist/esm/__tests__/services/cost/cost-calculator-service.spec.js.map +1 -0
  439. package/dist/esm/__tests__/services/cost/cost-currency-service.spec.d.ts +1 -0
  440. package/dist/esm/__tests__/services/cost/cost-currency-service.spec.js +113 -0
  441. package/dist/esm/__tests__/services/cost/cost-currency-service.spec.js.map +1 -0
  442. package/dist/esm/__tests__/services/cost/cost-filter-service.spec.d.ts +1 -0
  443. package/dist/esm/__tests__/services/cost/cost-filter-service.spec.js +467 -0
  444. package/dist/esm/__tests__/services/cost/cost-filter-service.spec.js.map +1 -0
  445. package/dist/esm/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.d.ts +1 -0
  446. package/dist/esm/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js +205 -0
  447. package/dist/esm/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js.map +1 -0
  448. package/dist/esm/__tests__/services/currency-exchange-rate-lookup-service.spec.d.ts +1 -0
  449. package/dist/esm/__tests__/services/currency-exchange-rate-lookup-service.spec.js +33 -0
  450. package/dist/esm/__tests__/services/currency-exchange-rate-lookup-service.spec.js.map +1 -0
  451. package/dist/esm/__tests__/services/email-verification/contact-email-verification-service.spec.d.ts +1 -1
  452. package/dist/esm/__tests__/services/email-verification/contact-email-verification-service.spec.js +91 -91
  453. package/dist/esm/__tests__/services/email-verification/email-verification-service.spec.d.ts +1 -1
  454. package/dist/esm/__tests__/services/email-verification/email-verification-service.spec.js +55 -55
  455. package/dist/esm/__tests__/shopify/shopify-graphql-transformer.spec.d.ts +1 -1
  456. package/dist/esm/__tests__/shopify/shopify-graphql-transformer.spec.js +33 -33
  457. package/dist/esm/__tests__/unit/libs/api-router/public-api-router.spec.d.ts +1 -1
  458. package/dist/esm/__tests__/unit/libs/api-router/public-api-router.spec.js +156 -156
  459. package/dist/esm/__tests__/unit/libs/api-router/route-matcher.spec.d.ts +1 -1
  460. package/dist/esm/__tests__/unit/libs/api-router/route-matcher.spec.js +67 -67
  461. package/dist/esm/__tests__/utils/custom-measure-formula-utils.spec.d.ts +1 -1
  462. package/dist/esm/__tests__/utils/custom-measure-formula-utils.spec.js +137 -137
  463. package/dist/esm/clients/generic/cognito-client.d.ts +23 -23
  464. package/dist/esm/clients/generic/cognito-client.js +204 -204
  465. package/dist/esm/clients/generic/dynamodb-client.d.ts +20 -20
  466. package/dist/esm/clients/generic/dynamodb-client.js +231 -231
  467. package/dist/esm/clients/generic/eventbridge-client.d.ts +14 -14
  468. package/dist/esm/clients/generic/eventbridge-client.js +47 -47
  469. package/dist/esm/clients/generic/http-client.d.ts +14 -14
  470. package/dist/esm/clients/generic/http-client.js +53 -53
  471. package/dist/esm/clients/generic/index.d.ts +13 -13
  472. package/dist/esm/clients/generic/index.js +13 -13
  473. package/dist/esm/clients/generic/lambda-invoke-client.d.ts +10 -10
  474. package/dist/esm/clients/generic/lambda-invoke-client.js +35 -35
  475. package/dist/esm/clients/generic/location-client.d.ts +8 -8
  476. package/dist/esm/clients/generic/location-client.js +27 -27
  477. package/dist/esm/clients/generic/redis-client.d.ts +33 -33
  478. package/dist/esm/clients/generic/redis-client.js +184 -184
  479. package/dist/esm/clients/generic/s3-client.d.ts +23 -23
  480. package/dist/esm/clients/generic/s3-client.js +209 -209
  481. package/dist/esm/clients/generic/singlestore-db-client.d.ts +14 -14
  482. package/dist/esm/clients/generic/singlestore-db-client.js +40 -40
  483. package/dist/esm/clients/generic/sqs-bundled-client.d.ts +15 -15
  484. package/dist/esm/clients/generic/sqs-bundled-client.js +307 -307
  485. package/dist/esm/clients/generic/sqs-bundled-client.types.d.ts +53 -53
  486. package/dist/esm/clients/generic/sqs-bundled-client.types.js +14 -14
  487. package/dist/esm/clients/generic/sqs-client.d.ts +53 -53
  488. package/dist/esm/clients/generic/sqs-client.js +281 -281
  489. package/dist/esm/clients/generic/sqs-unbundle.d.ts +32 -32
  490. package/dist/esm/clients/generic/sqs-unbundle.js +137 -137
  491. package/dist/esm/clients/index.d.ts +3 -3
  492. package/dist/esm/clients/index.js +3 -3
  493. package/dist/esm/clients/internal-api/accounts-client.d.ts +91 -91
  494. package/dist/esm/clients/internal-api/accounts-client.js +125 -125
  495. package/dist/esm/clients/internal-api/cache-lambda-client.d.ts +26 -26
  496. package/dist/esm/clients/internal-api/cache-lambda-client.js +85 -85
  497. package/dist/esm/clients/internal-api/db-management-client.d.ts +18 -18
  498. package/dist/esm/clients/internal-api/db-management-client.js +32 -32
  499. package/dist/esm/clients/internal-api/destinations-client.d.ts +34 -34
  500. package/dist/esm/clients/internal-api/destinations-client.js +75 -75
  501. package/dist/esm/clients/internal-api/event-collector-client.d.ts +20 -20
  502. package/dist/esm/clients/internal-api/event-collector-client.js +32 -32
  503. package/dist/esm/clients/internal-api/identity-client.d.ts +31 -31
  504. package/dist/esm/clients/internal-api/identity-client.js +87 -87
  505. package/dist/esm/clients/internal-api/index.d.ts +9 -9
  506. package/dist/esm/clients/internal-api/index.js +9 -9
  507. package/dist/esm/clients/internal-api/shopify-app-install-client.d.ts +37 -37
  508. package/dist/esm/clients/internal-api/shopify-app-install-client.js +77 -77
  509. package/dist/esm/clients/internal-api/subscriptions-client.d.ts +26 -26
  510. package/dist/esm/clients/internal-api/subscriptions-client.js +73 -73
  511. package/dist/esm/clients/internal-api/users-auth-client.d.ts +35 -35
  512. package/dist/esm/clients/internal-api/users-auth-client.js +106 -106
  513. package/dist/esm/clients/third-party/acuity-client.d.ts +10 -10
  514. package/dist/esm/clients/third-party/acuity-client.js +36 -36
  515. package/dist/esm/clients/third-party/emailable-client.d.ts +7 -7
  516. package/dist/esm/clients/third-party/emailable-client.js +21 -21
  517. package/dist/esm/clients/third-party/exchange-rate-api-client.d.ts +17 -17
  518. package/dist/esm/clients/third-party/exchange-rate-api-client.js +15 -15
  519. package/dist/esm/clients/third-party/index.d.ts +5 -5
  520. package/dist/esm/clients/third-party/index.js +5 -5
  521. package/dist/esm/clients/third-party/loops-client.d.ts +10 -10
  522. package/dist/esm/clients/third-party/loops-client.js +26 -26
  523. package/dist/esm/clients/third-party/shopify/graphql-order-queries.d.ts +25 -25
  524. package/dist/esm/clients/third-party/shopify/graphql-order-queries.js +1 -1
  525. package/dist/esm/clients/third-party/shopify/graphql-product-queries.d.ts +2 -2
  526. package/dist/esm/clients/third-party/shopify/graphql-product-queries.js +2 -2
  527. package/dist/esm/clients/third-party/shopify/shopify-graphql-client.d.ts +10 -10
  528. package/dist/esm/clients/third-party/shopify/shopify-graphql-client.js +157 -157
  529. package/dist/esm/clients/third-party/shopify-client.d.ts +29 -29
  530. package/dist/esm/clients/third-party/shopify-client.js +142 -142
  531. package/dist/esm/constants/index.d.ts +1 -1
  532. package/dist/esm/constants/index.js +1 -1
  533. package/dist/esm/constants/sqs.d.ts +20 -20
  534. package/dist/esm/constants/sqs.js +22 -22
  535. package/dist/esm/helpers/account-users-helper.d.ts +2 -2
  536. package/dist/esm/helpers/account-users-helper.js +18 -18
  537. package/dist/esm/helpers/acuity-helper.d.ts +4 -4
  538. package/dist/esm/helpers/acuity-helper.js +51 -51
  539. package/dist/esm/helpers/api-key-auth-helper.d.ts +9 -9
  540. package/dist/esm/helpers/api-key-auth-helper.js +35 -35
  541. package/dist/esm/helpers/api-key-authorizer-helper.d.ts +36 -36
  542. package/dist/esm/helpers/api-key-authorizer-helper.js +83 -83
  543. package/dist/esm/helpers/identity-cache-helper.d.ts +21 -21
  544. package/dist/esm/helpers/identity-cache-helper.js +152 -152
  545. package/dist/esm/helpers/index.d.ts +10 -10
  546. package/dist/esm/helpers/index.js +10 -10
  547. package/dist/esm/helpers/input-validation-helper.d.ts +3 -3
  548. package/dist/esm/helpers/input-validation-helper.js +18 -18
  549. package/dist/esm/helpers/logging-helper.d.ts +16 -16
  550. package/dist/esm/helpers/logging-helper.js +56 -56
  551. package/dist/esm/helpers/response-helper.d.ts +18 -18
  552. package/dist/esm/helpers/response-helper.js +37 -37
  553. package/dist/esm/helpers/shopify-helper.d.ts +9 -9
  554. package/dist/esm/helpers/shopify-helper.js +21 -21
  555. package/dist/esm/helpers/sqs-utils.d.ts +6 -6
  556. package/dist/esm/helpers/sqs-utils.js +9 -9
  557. package/dist/esm/index.d.ts +7 -7
  558. package/dist/esm/index.js +7 -7
  559. package/dist/esm/libs/api-router/index.d.ts +2 -2
  560. package/dist/esm/libs/api-router/index.js +2 -2
  561. package/dist/esm/libs/api-router/public-api-router.d.ts +3 -3
  562. package/dist/esm/libs/api-router/public-api-router.js +32 -32
  563. package/dist/esm/libs/api-router/route-matcher.d.ts +21 -21
  564. package/dist/esm/libs/api-router/route-matcher.js +30 -30
  565. package/dist/esm/libs/click-id-parser.d.ts +23 -23
  566. package/dist/esm/libs/click-id-parser.js +45 -45
  567. package/dist/esm/libs/compression.d.ts +2 -2
  568. package/dist/esm/libs/compression.js +25 -25
  569. package/dist/esm/libs/contacts.d.ts +7 -7
  570. package/dist/esm/libs/contacts.js +143 -143
  571. package/dist/esm/libs/cookie.d.ts +17 -17
  572. package/dist/esm/libs/cookie.js +70 -70
  573. package/dist/esm/libs/crypto.d.ts +4 -4
  574. package/dist/esm/libs/crypto.js +15 -15
  575. package/dist/esm/libs/csv.d.ts +2 -2
  576. package/dist/esm/libs/csv.js +30 -30
  577. package/dist/esm/libs/currency.d.ts +1 -1
  578. package/dist/esm/libs/currency.js +22 -22
  579. package/dist/esm/libs/dates.d.ts +12 -12
  580. package/dist/esm/libs/dates.js +83 -83
  581. package/dist/esm/libs/domain.d.ts +2 -2
  582. package/dist/esm/libs/domain.js +33 -33
  583. package/dist/esm/libs/emails.d.ts +8 -8
  584. package/dist/esm/libs/emails.js +146 -146
  585. package/dist/esm/libs/http-error.d.ts +21 -21
  586. package/dist/esm/libs/http-error.js +59 -59
  587. package/dist/esm/libs/http-status-codes.d.ts +58 -58
  588. package/dist/esm/libs/http-status-codes.js +59 -59
  589. package/dist/esm/libs/index.d.ts +19 -19
  590. package/dist/esm/libs/index.js +19 -19
  591. package/dist/esm/libs/numbers.d.ts +1 -1
  592. package/dist/esm/libs/numbers.js +11 -11
  593. package/dist/esm/libs/referrer-parser/index.d.ts +2 -2
  594. package/dist/esm/libs/referrer-parser/index.js +2 -2
  595. package/dist/esm/libs/referrer-parser/referrer-data.d.ts +9 -9
  596. package/dist/esm/libs/referrer-parser/referrer-data.js +3304 -3304
  597. package/dist/esm/libs/referrer-parser/referrer-parser-util.d.ts +20 -20
  598. package/dist/esm/libs/referrer-parser/referrer-parser-util.js +124 -124
  599. package/dist/esm/libs/strings.d.ts +3 -3
  600. package/dist/esm/libs/strings.js +40 -40
  601. package/dist/esm/libs/traits.d.ts +6 -6
  602. package/dist/esm/libs/traits.js +54 -54
  603. package/dist/esm/libs/url.d.ts +1 -1
  604. package/dist/esm/libs/url.js +9 -9
  605. package/dist/esm/services/acuity-api-service.d.ts +9 -9
  606. package/dist/esm/services/acuity-api-service.js +69 -69
  607. package/dist/esm/services/cache/generic-cached-object.d.ts +5 -5
  608. package/dist/esm/services/cache/generic-cached-object.js +1 -1
  609. package/dist/esm/services/cache/index.d.ts +1 -1
  610. package/dist/esm/services/cache/index.js +1 -1
  611. package/dist/esm/services/cache/product-cache-service.d.ts +21 -21
  612. package/dist/esm/services/cache/product-cache-service.js +68 -68
  613. package/dist/esm/services/cost/cost-calculation-types.d.ts +69 -0
  614. package/dist/esm/services/cost/cost-calculation-types.js +16 -0
  615. package/dist/esm/services/cost/cost-calculation-types.js.map +1 -0
  616. package/dist/esm/services/cost/cost-calculator-service.d.ts +24 -0
  617. package/dist/esm/services/cost/cost-calculator-service.js +451 -0
  618. package/dist/esm/services/cost/cost-calculator-service.js.map +1 -0
  619. package/dist/esm/services/cost/cost-currency-service.d.ts +6 -0
  620. package/dist/esm/services/cost/cost-currency-service.js +85 -0
  621. package/dist/esm/services/cost/cost-currency-service.js.map +1 -0
  622. package/dist/esm/services/cost/cost-filter-service.d.ts +10 -0
  623. package/dist/esm/services/cost/cost-filter-service.js +119 -0
  624. package/dist/esm/services/cost/cost-filter-service.js.map +1 -0
  625. package/dist/esm/services/cost/index.d.ts +5 -0
  626. package/dist/esm/services/cost/index.js +6 -0
  627. package/dist/esm/services/cost/index.js.map +1 -0
  628. package/dist/esm/services/cost/order-cost/index.d.ts +2 -0
  629. package/dist/esm/services/cost/order-cost/index.js +3 -0
  630. package/dist/esm/services/cost/order-cost/index.js.map +1 -0
  631. package/dist/esm/services/cost/order-cost/order-cost-resolution-service.d.ts +23 -0
  632. package/dist/esm/services/cost/order-cost/order-cost-resolution-service.js +356 -0
  633. package/dist/esm/services/cost/order-cost/order-cost-resolution-service.js.map +1 -0
  634. package/dist/esm/services/cost/order-cost/order-cost-resolution-types.d.ts +37 -0
  635. package/dist/esm/services/cost/order-cost/order-cost-resolution-types.js +2 -0
  636. package/dist/esm/services/cost/order-cost/order-cost-resolution-types.js.map +1 -0
  637. package/dist/esm/services/currency-exchange-rate-lookup-service.d.ts +12 -11
  638. package/dist/esm/services/currency-exchange-rate-lookup-service.js +90 -62
  639. package/dist/esm/services/currency-exchange-rate-lookup-service.js.map +1 -1
  640. package/dist/esm/services/db/accounts-db-service.d.ts +9 -9
  641. package/dist/esm/services/db/accounts-db-service.js +29 -29
  642. package/dist/esm/services/db/api-keys-db-service.d.ts +10 -10
  643. package/dist/esm/services/db/api-keys-db-service.js +32 -32
  644. package/dist/esm/services/db/contact-enrichments-db-service.d.ts +15 -15
  645. package/dist/esm/services/db/contact-enrichments-db-service.js +90 -90
  646. package/dist/esm/services/db/currency-exchange-rates-db-service.d.ts +21 -21
  647. package/dist/esm/services/db/currency-exchange-rates-db-service.js +35 -35
  648. package/dist/esm/services/db/custom-measures-db-service.d.ts +14 -14
  649. package/dist/esm/services/db/custom-measures-db-service.js +44 -44
  650. package/dist/esm/services/db/destinations-db-service.d.ts +13 -13
  651. package/dist/esm/services/db/destinations-db-service.js +70 -70
  652. package/dist/esm/services/db/identity-cache-db-service.d.ts +28 -28
  653. package/dist/esm/services/db/identity-cache-db-service.js +313 -313
  654. package/dist/esm/services/db/identity-cache-dynamodb-service.d.ts +38 -34
  655. package/dist/esm/services/db/identity-cache-dynamodb-service.js +432 -426
  656. package/dist/esm/services/db/identity-cache-dynamodb-service.js.map +1 -1
  657. package/dist/esm/services/db/index.d.ts +19 -17
  658. package/dist/esm/services/db/index.js +19 -17
  659. package/dist/esm/services/db/index.js.map +1 -1
  660. package/dist/esm/services/db/log-events-db-service.d.ts +11 -11
  661. package/dist/esm/services/db/log-events-db-service.js +177 -177
  662. package/dist/esm/services/db/pixels-db-service.d.ts +8 -8
  663. package/dist/esm/services/db/pixels-db-service.js +31 -31
  664. package/dist/esm/services/db/products-db-service-types.d.ts +10 -0
  665. package/dist/esm/services/db/products-db-service-types.js +2 -0
  666. package/dist/esm/services/db/products-db-service-types.js.map +1 -0
  667. package/dist/esm/services/db/products-db-service.d.ts +19 -0
  668. package/dist/esm/services/db/products-db-service.js +278 -0
  669. package/dist/esm/services/db/products-db-service.js.map +1 -0
  670. package/dist/esm/services/db/purchasable-contacts-db-service.d.ts +9 -9
  671. package/dist/esm/services/db/purchasable-contacts-db-service.js +39 -39
  672. package/dist/esm/services/db/purchased-contacts/index.d.ts +2 -2
  673. package/dist/esm/services/db/purchased-contacts/index.js +2 -2
  674. package/dist/esm/services/db/purchased-contacts/purchased-contacts-db-service.d.ts +18 -18
  675. package/dist/esm/services/db/purchased-contacts/purchased-contacts-db-service.js +148 -148
  676. package/dist/esm/services/db/purchased-contacts/types.d.ts +11 -11
  677. package/dist/esm/services/db/purchased-contacts/types.js +1 -1
  678. package/dist/esm/services/db/shopify-app-installs-db-service.d.ts +10 -10
  679. package/dist/esm/services/db/shopify-app-installs-db-service.js +48 -48
  680. package/dist/esm/services/db/shopify-products-cache-db-service.d.ts +16 -16
  681. package/dist/esm/services/db/shopify-products-cache-db-service.js +66 -66
  682. package/dist/esm/services/db/subscriptions-db-service.d.ts +11 -11
  683. package/dist/esm/services/db/subscriptions-db-service.js +34 -34
  684. package/dist/esm/services/db/tracking-events-db-service.d.ts +21 -21
  685. package/dist/esm/services/db/tracking-events-db-service.js +184 -184
  686. package/dist/esm/services/db/user-accounts-db-service.d.ts +7 -7
  687. package/dist/esm/services/db/user-accounts-db-service.js +13 -13
  688. package/dist/esm/services/email-verification/contact-email-verification-service.d.ts +7 -7
  689. package/dist/esm/services/email-verification/contact-email-verification-service.js +97 -97
  690. package/dist/esm/services/email-verification/email-verification-service.d.ts +19 -19
  691. package/dist/esm/services/email-verification/email-verification-service.js +127 -127
  692. package/dist/esm/services/email-verification/index.d.ts +2 -2
  693. package/dist/esm/services/email-verification/index.js +2 -2
  694. package/dist/esm/services/eventbridge-integration-service.d.ts +9 -9
  695. package/dist/esm/services/eventbridge-integration-service.js +24 -24
  696. package/dist/esm/services/events/index.d.ts +3 -3
  697. package/dist/esm/services/events/index.js +3 -3
  698. package/dist/esm/services/events/log-event-service.d.ts +19 -19
  699. package/dist/esm/services/events/log-event-service.js +73 -73
  700. package/dist/esm/services/events/metric-event-service.d.ts +9 -9
  701. package/dist/esm/services/events/metric-event-service.js +45 -45
  702. package/dist/esm/services/events/tracking-event-sqs-service.d.ts +8 -8
  703. package/dist/esm/services/events/tracking-event-sqs-service.js +30 -30
  704. package/dist/esm/services/generic-cache-service.d.ts +7 -7
  705. package/dist/esm/services/generic-cache-service.js +29 -29
  706. package/dist/esm/services/index.d.ts +11 -10
  707. package/dist/esm/services/index.js +11 -10
  708. package/dist/esm/services/index.js.map +1 -1
  709. package/dist/esm/services/ipdata-lookup-service.d.ts +20 -20
  710. package/dist/esm/services/ipdata-lookup-service.js +108 -108
  711. package/dist/esm/services/shopify/index.d.ts +2 -2
  712. package/dist/esm/services/shopify/index.js +2 -2
  713. package/dist/esm/services/shopify/products/index.d.ts +1 -1
  714. package/dist/esm/services/shopify/products/index.js +1 -1
  715. package/dist/esm/services/shopify/products/shopify-products-serviceV2.d.ts +17 -17
  716. package/dist/esm/services/shopify/products/shopify-products-serviceV2.js +108 -108
  717. package/dist/esm/services/shopify/shopify-graphql-transformer.d.ts +8 -8
  718. package/dist/esm/services/shopify/shopify-graphql-transformer.js +138 -138
  719. package/dist/esm/types/acuity-types.d.ts +74 -74
  720. package/dist/esm/types/acuity-types.js +1 -1
  721. package/dist/esm/types/api-response.d.ts +6 -6
  722. package/dist/esm/types/api-response.js +1 -1
  723. package/dist/esm/types/index.d.ts +4 -4
  724. package/dist/esm/types/index.js +4 -4
  725. package/dist/esm/types/internal-events/event-detail-types.d.ts +20 -20
  726. package/dist/esm/types/internal-events/event-detail-types.js +24 -24
  727. package/dist/esm/types/internal-events/index.d.ts +1 -1
  728. package/dist/esm/types/internal-events/index.js +1 -1
  729. package/dist/esm/types/shopify-graphql-types/admin.generated.d.ts +123 -123
  730. package/dist/esm/types/shopify-graphql-types/admin.generated.js +1 -1
  731. package/dist/esm/types/shopify-graphql-types/admin.types.d.ts +26289 -26289
  732. package/dist/esm/types/shopify-graphql-types/admin.types.js +5299 -5299
  733. package/dist/esm/types/shopify-graphql-types/index.d.ts +2 -2
  734. package/dist/esm/types/shopify-graphql-types/index.js +2 -2
  735. package/dist/esm/types/shopify-rest-types.d.ts +767 -767
  736. package/dist/esm/types/shopify-rest-types.js +1 -1
  737. package/dist/esm/utils/compression.d.ts +36 -36
  738. package/dist/esm/utils/compression.js +187 -187
  739. package/dist/esm/utils/custom-measure-formula-utils.d.ts +6 -6
  740. package/dist/esm/utils/custom-measure-formula-utils.js +201 -201
  741. package/dist/esm/utils/index.d.ts +4 -4
  742. package/dist/esm/utils/index.js +4 -4
  743. package/dist/esm/utils/retry-envelope.d.ts +12 -12
  744. package/dist/esm/utils/retry-envelope.js +22 -22
  745. package/dist/esm/utils/size.d.ts +2 -2
  746. package/dist/esm/utils/size.js +44 -44
  747. package/package.json +134 -134
@@ -0,0 +1,3318 @@
1
+ import { ORDER_COST_ENTRY_TYPE, SHIPPING_METHOD, SHIPPING_RATE_TYPE, WEIGHT_UNIT, COST_FILTER_MODE, } from '@adtrackify/at-tracking-event-types';
2
+ import { CostCalculatorService } from '../../../services/cost/cost-calculator-service';
3
+ jest.mock('../../../helpers/logging-helper', () => ({
4
+ Logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
5
+ }));
6
+ const emptyRatesMap = {};
7
+ const createLineItem = (overrides = {}) => ({
8
+ lineItemId: '1',
9
+ quantity: 1,
10
+ refundedQuantity: 0,
11
+ filterContext: {
12
+ tags: [],
13
+ productId: '100',
14
+ variantId: '200',
15
+ },
16
+ ...overrides,
17
+ });
18
+ const createContext = (overrides = {}) => ({
19
+ orderCreatedAt: '2024-06-15T10:00:00Z',
20
+ totalPrice: 100,
21
+ grossSalePrice: 120,
22
+ netSalePrice: 90,
23
+ totalProductCost: 40,
24
+ totalShipping: 10,
25
+ totalWeight: 500,
26
+ totalQuantity: 3,
27
+ hasRefund: false,
28
+ orderFilterContext: {
29
+ tags: [],
30
+ paymentGateways: ['stripe'],
31
+ shippingTitles: ['Standard'],
32
+ },
33
+ lineItems: [createLineItem()],
34
+ ...overrides,
35
+ });
36
+ const createEntry = (type, overrides = {}) => ({
37
+ id: 'e1',
38
+ name: 'Test Entry',
39
+ type,
40
+ flatAmount: 5,
41
+ percentRate: 0,
42
+ currency: 'USD',
43
+ category: 'ops',
44
+ channel: 'all',
45
+ createdAt: '2024-01-01T00:00:00Z',
46
+ updatedAt: '2024-01-01T00:00:00Z',
47
+ ...overrides,
48
+ });
49
+ const createConfigs = (overrides = {}) => ({
50
+ globalProductOverride: null,
51
+ order: null,
52
+ gateway: null,
53
+ shipping: null,
54
+ ...overrides,
55
+ });
56
+ describe('CostCalculatorService', () => {
57
+ describe('isDateInRange', () => {
58
+ it('should return true when no date bounds', () => {
59
+ expect(CostCalculatorService.isDateInRange('2024-06-15T10:00:00Z')).toBe(true);
60
+ });
61
+ it('should return true when order date is within range', () => {
62
+ expect(CostCalculatorService.isDateInRange('2024-06-15T10:00:00Z', '2024-01-01T00:00:00Z', '2024-12-31T23:59:59Z')).toBe(true);
63
+ });
64
+ it('should return false when order date is before effectiveFrom', () => {
65
+ expect(CostCalculatorService.isDateInRange('2023-06-15T10:00:00Z', '2024-01-01T00:00:00Z')).toBe(false);
66
+ });
67
+ it('should return false when order date is after effectiveTo', () => {
68
+ expect(CostCalculatorService.isDateInRange('2025-06-15T10:00:00Z', undefined, '2024-12-31T23:59:59Z')).toBe(false);
69
+ });
70
+ it('should return true for invalid order date', () => {
71
+ expect(CostCalculatorService.isDateInRange('invalid-date', '2024-01-01T00:00:00Z')).toBe(true);
72
+ });
73
+ });
74
+ describe('findMatchingWeightTierAmount', () => {
75
+ it('should return 0 for empty tiers', () => {
76
+ expect(CostCalculatorService.findMatchingWeightTierAmount([], 500, undefined)).toBe(0);
77
+ expect(CostCalculatorService.findMatchingWeightTierAmount(undefined, 500, undefined)).toBe(0);
78
+ });
79
+ it('should find correct tier', () => {
80
+ const tiers = [
81
+ { weightMin: 0, weightMax: 500, flatAmount: 5 },
82
+ { weightMin: 501, weightMax: 1000, flatAmount: 10 },
83
+ { weightMin: 1001, weightMax: null, flatAmount: 15 },
84
+ ];
85
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 250, undefined)).toBe(5);
86
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 750, undefined)).toBe(10);
87
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 2000, undefined)).toBe(15);
88
+ });
89
+ it('should return 0 when no tier matches', () => {
90
+ const tiers = [{ weightMin: 100, weightMax: 200, flatAmount: 5 }];
91
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 50, undefined)).toBe(0);
92
+ });
93
+ it('should convert weight from grams to OUNCES before matching', () => {
94
+ const tiers = [
95
+ { weightMin: 0, weightMax: 5, flatAmount: 2 },
96
+ { weightMin: 5.01, weightMax: null, flatAmount: 8 },
97
+ ];
98
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 100, WEIGHT_UNIT.OUNCES)).toBe(2);
99
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 500, WEIGHT_UNIT.OUNCES)).toBe(8);
100
+ });
101
+ it('should return 0 when tier has null flatAmount', () => {
102
+ const tiers = [{ weightMin: 0, weightMax: 1000, flatAmount: null }];
103
+ expect(CostCalculatorService.findMatchingWeightTierAmount(tiers, 500, undefined)).toBe(0);
104
+ });
105
+ });
106
+ describe('findQuantityTierAmount', () => {
107
+ it('should return 0 for empty tiers', () => {
108
+ expect(CostCalculatorService.findQuantityTierAmount([], 5)).toBe(0);
109
+ expect(CostCalculatorService.findQuantityTierAmount(undefined, 5)).toBe(0);
110
+ });
111
+ it('should find correct tier', () => {
112
+ const tiers = [
113
+ { quantityMin: 1, quantityMax: 10, flatAmount: 2 },
114
+ { quantityMin: 11, quantityMax: null, flatAmount: 1 },
115
+ ];
116
+ expect(CostCalculatorService.findQuantityTierAmount(tiers, 5)).toBe(2);
117
+ expect(CostCalculatorService.findQuantityTierAmount(tiers, 20)).toBe(1);
118
+ });
119
+ it('should return 0 when quantity is below all tiers', () => {
120
+ const tiers = [
121
+ { quantityMin: 5, quantityMax: 10, flatAmount: 2 },
122
+ { quantityMin: 11, quantityMax: null, flatAmount: 1 },
123
+ ];
124
+ expect(CostCalculatorService.findQuantityTierAmount(tiers, 2)).toBe(0);
125
+ });
126
+ it('should return 0 when tier has null flatAmount', () => {
127
+ const tiers = [{ quantityMin: 1, quantityMax: 10, flatAmount: null }];
128
+ expect(CostCalculatorService.findQuantityTierAmount(tiers, 5)).toBe(0);
129
+ });
130
+ });
131
+ describe('calculateHandlingCost', () => {
132
+ const initLineItemCosts = (lineItemIds = ['1']) => {
133
+ const result = {};
134
+ for (const id of lineItemIds) {
135
+ result[id] = { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 };
136
+ }
137
+ return result;
138
+ };
139
+ it('should return 0 when no globalProductOverride', () => {
140
+ const configs = createConfigs();
141
+ const lineItems = [createLineItem({ quantity: 1 })];
142
+ const liCosts = initLineItemCosts();
143
+ expect(CostCalculatorService.calculateHandlingCost(configs, lineItems, liCosts, 'USD', emptyRatesMap)).toBe(0);
144
+ });
145
+ it('should apply flat fee per line item (not multiplied by quantity)', () => {
146
+ const configs = createConfigs({
147
+ globalProductOverride: { globalHandlingFee: 2, currency: 'USD' },
148
+ });
149
+ const lineItems = [createLineItem({ lineItemId: '1', quantity: 3 })];
150
+ const liCosts = initLineItemCosts();
151
+ const result = CostCalculatorService.calculateHandlingCost(configs, lineItems, liCosts, 'USD', emptyRatesMap);
152
+ expect(result).toBe(2);
153
+ expect(liCosts['1'].totalLineItemHandlingCost).toBe(2);
154
+ });
155
+ it('should sum across multiple line items', () => {
156
+ const configs = createConfigs({
157
+ globalProductOverride: { globalHandlingFee: 2, currency: 'USD' },
158
+ });
159
+ const lineItems = [
160
+ createLineItem({ lineItemId: '1', quantity: 2 }),
161
+ createLineItem({ lineItemId: '2', quantity: 3 }),
162
+ ];
163
+ const liCosts = initLineItemCosts(['1', '2']);
164
+ const result = CostCalculatorService.calculateHandlingCost(configs, lineItems, liCosts, 'USD', emptyRatesMap);
165
+ expect(result).toBe(4);
166
+ expect(liCosts['1'].totalLineItemHandlingCost).toBe(2);
167
+ expect(liCosts['2'].totalLineItemHandlingCost).toBe(2);
168
+ });
169
+ it('should convert currency when config currency differs from order currency', () => {
170
+ const configs = createConfigs({
171
+ globalProductOverride: { globalHandlingFee: 10, currency: 'EUR' },
172
+ });
173
+ const rates = { EUR: { USD: 1.1 } };
174
+ const lineItems = [createLineItem({ quantity: 1 })];
175
+ const liCosts = initLineItemCosts();
176
+ const result = CostCalculatorService.calculateHandlingCost(configs, lineItems, liCosts, 'USD', rates);
177
+ expect(result).toBeCloseTo(11);
178
+ });
179
+ it('should return fee without conversion when config has no currency', () => {
180
+ const configs = createConfigs({
181
+ globalProductOverride: { globalHandlingFee: 5 },
182
+ });
183
+ const lineItems = [createLineItem({ quantity: 1 })];
184
+ const liCosts = initLineItemCosts();
185
+ expect(CostCalculatorService.calculateHandlingCost(configs, lineItems, liCosts, 'USD', emptyRatesMap)).toBe(5);
186
+ });
187
+ it('should add global handling fee on top of existing per-variant handling cost', () => {
188
+ const configs = createConfigs({
189
+ globalProductOverride: { globalHandlingFee: 2, currency: 'USD' },
190
+ });
191
+ const lineItems = [createLineItem({ lineItemId: '1', quantity: 3 })];
192
+ const liCosts = initLineItemCosts();
193
+ liCosts['1'].totalLineItemHandlingCost = 4.5;
194
+ CostCalculatorService.calculateHandlingCost(configs, lineItems, liCosts, 'USD', emptyRatesMap);
195
+ expect(liCosts['1'].totalLineItemHandlingCost).toBe(6.5);
196
+ });
197
+ });
198
+ describe('calculateGatewayCost', () => {
199
+ it('should return 0 when no gateway config', () => {
200
+ const configs = createConfigs();
201
+ expect(CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(0);
202
+ });
203
+ it('should return 0 when no matching gateway', () => {
204
+ const configs = createConfigs({
205
+ gateway: {
206
+ entries: [{ gatewayName: 'paypal', flatAmount: 0.30, percentRate: 2.9, currency: 'USD', createdAt: '', updatedAt: '' }],
207
+ },
208
+ });
209
+ expect(CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(0);
210
+ });
211
+ it('should calculate flat + percent for matching gateway', () => {
212
+ const configs = createConfigs({
213
+ gateway: {
214
+ entries: [{ gatewayName: 'stripe', flatAmount: 0.30, percentRate: 2.9, currency: 'USD', createdAt: '', updatedAt: '' }],
215
+ },
216
+ });
217
+ const result = CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap);
218
+ expect(result).toBeCloseTo(3.20);
219
+ });
220
+ it('should be case-insensitive for gateway name', () => {
221
+ const configs = createConfigs({
222
+ gateway: {
223
+ entries: [{ gatewayName: 'Stripe', flatAmount: 1, percentRate: 0, currency: 'USD', createdAt: '', updatedAt: '' }],
224
+ },
225
+ });
226
+ expect(CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(1);
227
+ });
228
+ it('should respect date range', () => {
229
+ const configs = createConfigs({
230
+ gateway: {
231
+ entries: [{
232
+ gatewayName: 'stripe', flatAmount: 1, percentRate: 0, currency: 'USD',
233
+ effectiveFrom: '2025-01-01T00:00:00Z', createdAt: '', updatedAt: '',
234
+ }],
235
+ },
236
+ });
237
+ expect(CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(0);
238
+ });
239
+ it('should convert flat amount from entry currency', () => {
240
+ const configs = createConfigs({
241
+ gateway: {
242
+ entries: [{ gatewayName: 'stripe', flatAmount: 1, percentRate: 0, currency: 'EUR', createdAt: '', updatedAt: '' }],
243
+ },
244
+ });
245
+ const rates = { EUR: { USD: 1.1 } };
246
+ const result = CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', rates);
247
+ expect(result).toBeCloseTo(1.1);
248
+ });
249
+ it('should pick first matching gateway when multiple entries exist', () => {
250
+ const configs = createConfigs({
251
+ gateway: {
252
+ entries: [
253
+ { gatewayName: 'stripe', flatAmount: 0.30, percentRate: 2.9, currency: 'USD', createdAt: '', updatedAt: '' },
254
+ { gatewayName: 'stripe', flatAmount: 0.50, percentRate: 3.5, currency: 'USD', createdAt: '', updatedAt: '' },
255
+ ],
256
+ },
257
+ });
258
+ const result = CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap);
259
+ expect(result).toBeCloseTo(3.20);
260
+ });
261
+ it('should respect effectiveTo date on gateway entry', () => {
262
+ const configs = createConfigs({
263
+ gateway: {
264
+ entries: [{
265
+ gatewayName: 'stripe', flatAmount: 1, percentRate: 0, currency: 'USD',
266
+ effectiveTo: '2024-01-01T00:00:00Z', createdAt: '', updatedAt: '',
267
+ }],
268
+ },
269
+ });
270
+ expect(CostCalculatorService.calculateGatewayCost(configs, ['stripe'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(0);
271
+ });
272
+ it('should match gateway when order has multiple gateways', () => {
273
+ const configs = createConfigs({
274
+ gateway: {
275
+ entries: [{ gatewayName: 'paypal', flatAmount: 0.50, percentRate: 3.0, currency: 'USD', createdAt: '', updatedAt: '' }],
276
+ },
277
+ });
278
+ const result = CostCalculatorService.calculateGatewayCost(configs, ['stripe', 'paypal'], 100, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap);
279
+ expect(result).toBeCloseTo(3.50);
280
+ });
281
+ });
282
+ describe('calculateShippingCost', () => {
283
+ it('should return 0 when no shipping config', () => {
284
+ const configs = createConfigs();
285
+ expect(CostCalculatorService.calculateShippingCost(configs, 10, 'US', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(0);
286
+ });
287
+ it('should return shopify amount for SHOPIFY_CHARGES method', () => {
288
+ const configs = createConfigs({
289
+ shipping: { method: SHIPPING_METHOD.SHOPIFY_CHARGES, currency: 'USD' },
290
+ });
291
+ expect(CostCalculatorService.calculateShippingCost(configs, 15.50, 'US', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(15.50);
292
+ });
293
+ it('should return fixed rate for FIXED_RATE method', () => {
294
+ const configs = createConfigs({
295
+ shipping: { method: SHIPPING_METHOD.FIXED_RATE, fixedRateAmount: 8.99, currency: 'USD' },
296
+ });
297
+ expect(CostCalculatorService.calculateShippingCost(configs, 10, 'US', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(8.99);
298
+ });
299
+ it('should convert fixed rate from shipping currency', () => {
300
+ const configs = createConfigs({
301
+ shipping: { method: SHIPPING_METHOD.FIXED_RATE, fixedRateAmount: 10, currency: 'GBP' },
302
+ });
303
+ const rates = { GBP: { USD: 1.27 } };
304
+ const result = CostCalculatorService.calculateShippingCost(configs, 0, 'US', 500, '2024-06-15T10:00:00Z', 'USD', rates);
305
+ expect(result).toBeCloseTo(12.7);
306
+ });
307
+ it('should match shipping profile by country', () => {
308
+ const configs = createConfigs({
309
+ shipping: {
310
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
311
+ currency: 'USD',
312
+ profiles: [
313
+ {
314
+ id: 'p1', name: 'US Profile', countries: ['US'], isDefault: false,
315
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 5, currency: 'USD',
316
+ createdAt: '', updatedAt: '',
317
+ },
318
+ {
319
+ id: 'p2', name: 'Default', countries: [], isDefault: true,
320
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 15, currency: 'USD',
321
+ createdAt: '', updatedAt: '',
322
+ },
323
+ ],
324
+ },
325
+ });
326
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'US', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(5);
327
+ });
328
+ it('should fall back to default profile', () => {
329
+ const configs = createConfigs({
330
+ shipping: {
331
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
332
+ currency: 'USD',
333
+ profiles: [
334
+ {
335
+ id: 'p1', name: 'US Profile', countries: ['US'], isDefault: false,
336
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 5, currency: 'USD',
337
+ createdAt: '', updatedAt: '',
338
+ },
339
+ {
340
+ id: 'p2', name: 'Default', countries: [], isDefault: true,
341
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 15, currency: 'USD',
342
+ createdAt: '', updatedAt: '',
343
+ },
344
+ ],
345
+ },
346
+ });
347
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'CA', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(15);
348
+ });
349
+ it('should handle weight-tiered profile', () => {
350
+ const configs = createConfigs({
351
+ shipping: {
352
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
353
+ currency: 'USD',
354
+ profiles: [{
355
+ id: 'p1', name: 'Weight', countries: ['US'], isDefault: false,
356
+ rateType: SHIPPING_RATE_TYPE.WEIGHT_TIERED, currency: 'USD',
357
+ weightUnit: WEIGHT_UNIT.GRAMS,
358
+ weightTiers: [
359
+ { weightMin: 0, weightMax: 500, flatAmount: 5 },
360
+ { weightMin: 501, weightMax: null, flatAmount: 10 },
361
+ ],
362
+ createdAt: '', updatedAt: '',
363
+ }],
364
+ },
365
+ });
366
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'US', 300, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(5);
367
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'US', 700, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(10);
368
+ });
369
+ it('should return 0 when no shipping profile matches (no country match, no default)', () => {
370
+ const configs = createConfigs({
371
+ shipping: {
372
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
373
+ currency: 'USD',
374
+ profiles: [{
375
+ id: 'p1', name: 'US Only', countries: ['US'], isDefault: false,
376
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 5, currency: 'USD',
377
+ createdAt: '', updatedAt: '',
378
+ }],
379
+ },
380
+ });
381
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'CA', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(0);
382
+ });
383
+ it('should skip expired shipping profile and fall back to default', () => {
384
+ const configs = createConfigs({
385
+ shipping: {
386
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
387
+ currency: 'USD',
388
+ profiles: [
389
+ {
390
+ id: 'p1', name: 'US Expired', countries: ['US'], isDefault: false,
391
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 5, currency: 'USD',
392
+ effectiveTo: '2024-01-01T00:00:00Z',
393
+ createdAt: '', updatedAt: '',
394
+ },
395
+ {
396
+ id: 'p2', name: 'Default', countries: [], isDefault: true,
397
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 12, currency: 'USD',
398
+ createdAt: '', updatedAt: '',
399
+ },
400
+ ],
401
+ },
402
+ });
403
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'US', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(12);
404
+ });
405
+ it('should convert weight-tiered shipping profile amount from profile currency', () => {
406
+ const configs = createConfigs({
407
+ shipping: {
408
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
409
+ currency: 'EUR',
410
+ profiles: [{
411
+ id: 'p1', name: 'EU Weight', countries: ['DE'], isDefault: false,
412
+ rateType: SHIPPING_RATE_TYPE.WEIGHT_TIERED, currency: 'EUR',
413
+ weightUnit: WEIGHT_UNIT.GRAMS,
414
+ weightTiers: [
415
+ { weightMin: 0, weightMax: 500, flatAmount: 5 },
416
+ { weightMin: 501, weightMax: null, flatAmount: 10 },
417
+ ],
418
+ createdAt: '', updatedAt: '',
419
+ }],
420
+ },
421
+ });
422
+ const rates = { EUR: { USD: 1.1 } };
423
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'DE', 300, '2024-06-15T10:00:00Z', 'USD', rates)).toBeCloseTo(5.5);
424
+ });
425
+ it('should be case-insensitive for shipping profile country matching', () => {
426
+ const configs = createConfigs({
427
+ shipping: {
428
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
429
+ currency: 'USD',
430
+ profiles: [{
431
+ id: 'p1', name: 'US', countries: ['us'], isDefault: false,
432
+ rateType: SHIPPING_RATE_TYPE.FLAT, flatAmount: 7, currency: 'USD',
433
+ createdAt: '', updatedAt: '',
434
+ }],
435
+ },
436
+ });
437
+ expect(CostCalculatorService.calculateShippingCost(configs, 0, 'US', 500, '2024-06-15T10:00:00Z', 'USD', emptyRatesMap)).toBe(7);
438
+ });
439
+ });
440
+ describe('calculateOtherCostEntry', () => {
441
+ const initLineItemCosts = () => ({
442
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
443
+ });
444
+ it('should calculate PER_ORDER flat amount as order-level cost', () => {
445
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 3 });
446
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext(), initLineItemCosts(), 'USD', emptyRatesMap);
447
+ expect(result.orderLevel).toBe(3);
448
+ expect(result.lineItemLevel).toBe(0);
449
+ });
450
+ it('should calculate PER_LINE_ITEM with quantity as line-item-level cost', () => {
451
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 2 });
452
+ const liCosts = initLineItemCosts();
453
+ const context = createContext({ lineItems: [createLineItem({ quantity: 3 })] });
454
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
455
+ expect(result.orderLevel).toBe(0);
456
+ expect(result.lineItemLevel).toBe(6);
457
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
458
+ });
459
+ it('should accumulate PER_LINE_ITEM costs across multiple line items as line-item-level', () => {
460
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 2 });
461
+ const liCosts = {
462
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
463
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
464
+ '3': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
465
+ };
466
+ const context = createContext({
467
+ lineItems: [
468
+ createLineItem({ lineItemId: '1', quantity: 3 }),
469
+ createLineItem({ lineItemId: '2', quantity: 1 }),
470
+ createLineItem({ lineItemId: '3', quantity: 5 }),
471
+ ],
472
+ });
473
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
474
+ expect(result.orderLevel).toBe(0);
475
+ expect(result.lineItemLevel).toBe(18);
476
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
477
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(2);
478
+ expect(liCosts['3'].totalLineItemOtherCost).toBe(10);
479
+ });
480
+ it('should calculate BY_ORDER_WEIGHT with flat amount as order-level cost', () => {
481
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, { flatAmount: 0.01 });
482
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 500 }), initLineItemCosts(), 'USD', emptyRatesMap);
483
+ expect(result.orderLevel).toBe(5);
484
+ expect(result.lineItemLevel).toBe(0);
485
+ });
486
+ it('should calculate BY_ORDER_WEIGHT with weight tiers in KILOGRAMS (totalWeight is grams from Shopify)', () => {
487
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
488
+ weightUnit: WEIGHT_UNIT.KILOGRAMS,
489
+ weightIntervals: [
490
+ { weightMin: 0, weightMax: 5, flatAmount: 3 },
491
+ { weightMin: 5.01, weightMax: 20, flatAmount: 8 },
492
+ { weightMin: 20.01, weightMax: null, flatAmount: 15 },
493
+ ],
494
+ });
495
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 2000 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(3);
496
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 10000 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(8);
497
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 25000 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(15);
498
+ });
499
+ it('should calculate BY_ORDER_WEIGHT with weight tiers in POUNDS', () => {
500
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
501
+ weightUnit: WEIGHT_UNIT.POUNDS,
502
+ weightIntervals: [
503
+ { weightMin: 0, weightMax: 10, flatAmount: 5 },
504
+ { weightMin: 10.01, weightMax: null, flatAmount: 12 },
505
+ ],
506
+ });
507
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 2000 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
508
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 10000 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(12);
509
+ });
510
+ it('should calculate BY_ORDER_WEIGHT with weight tiers in GRAMS (no conversion needed)', () => {
511
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
512
+ weightUnit: WEIGHT_UNIT.GRAMS,
513
+ weightIntervals: [
514
+ { weightMin: 0, weightMax: 1000, flatAmount: 2 },
515
+ { weightMin: 1001, weightMax: null, flatAmount: 7 },
516
+ ],
517
+ });
518
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 500 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(2);
519
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 1500 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(7);
520
+ });
521
+ it('should calculate BY_ORDER_WEIGHT with weight tiers and no weightUnit (treats as grams)', () => {
522
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
523
+ weightUnit: undefined,
524
+ weightIntervals: [
525
+ { weightMin: 0, weightMax: 1000, flatAmount: 4 },
526
+ { weightMin: 1001, weightMax: null, flatAmount: 9 },
527
+ ],
528
+ });
529
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 500 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(4);
530
+ });
531
+ it('should calculate BY_ORDER_WEIGHT with flat amount and KILOGRAMS unit (converts weight before multiplying)', () => {
532
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
533
+ flatAmount: 0.50,
534
+ weightUnit: WEIGHT_UNIT.KILOGRAMS,
535
+ });
536
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 2000 }), initLineItemCosts(), 'USD', emptyRatesMap);
537
+ expect(result.orderLevel).toBe(1);
538
+ expect(result.lineItemLevel).toBe(0);
539
+ });
540
+ it('should calculate BY_ORDER_WEIGHT with flat amount and POUNDS unit (converts weight before multiplying)', () => {
541
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
542
+ flatAmount: 2.00,
543
+ weightUnit: WEIGHT_UNIT.POUNDS,
544
+ });
545
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 453.592 }), initLineItemCosts(), 'USD', emptyRatesMap);
546
+ expect(result.orderLevel).toBeCloseTo(2);
547
+ expect(result.lineItemLevel).toBe(0);
548
+ });
549
+ it('should calculate BY_LINE_ITEM_QUANTITY with flat amount as line-item-level cost', () => {
550
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, { flatAmount: 0.50 });
551
+ const liCosts = initLineItemCosts();
552
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ lineItems: [createLineItem({ quantity: 10 })] }), liCosts, 'USD', emptyRatesMap);
553
+ expect(result.orderLevel).toBe(0);
554
+ expect(result.lineItemLevel).toBe(5);
555
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(5);
556
+ });
557
+ it('should calculate BY_LINE_ITEM_QUANTITY with tiers per line item quantity', () => {
558
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
559
+ quantityIntervals: [
560
+ { quantityMin: 1, quantityMax: 5, flatAmount: 3 },
561
+ { quantityMin: 6, quantityMax: null, flatAmount: 2 },
562
+ ],
563
+ });
564
+ const liCosts1 = initLineItemCosts();
565
+ const result1 = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ lineItems: [createLineItem({ quantity: 3 })] }), liCosts1, 'USD', emptyRatesMap);
566
+ expect(result1.lineItemLevel).toBe(3);
567
+ expect(result1.orderLevel).toBe(0);
568
+ const liCosts2 = initLineItemCosts();
569
+ const result2 = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ lineItems: [createLineItem({ quantity: 10 })] }), liCosts2, 'USD', emptyRatesMap);
570
+ expect(result2.lineItemLevel).toBe(2);
571
+ expect(result2.orderLevel).toBe(0);
572
+ });
573
+ it('should calculate BY_LINE_ITEM_QUANTITY with multiple line items at different tiers', () => {
574
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
575
+ quantityIntervals: [
576
+ { quantityMin: 1, quantityMax: 5, flatAmount: 3 },
577
+ { quantityMin: 6, quantityMax: null, flatAmount: 2 },
578
+ ],
579
+ });
580
+ const liCosts = {
581
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
582
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
583
+ };
584
+ const context = createContext({
585
+ lineItems: [
586
+ createLineItem({ lineItemId: '1', quantity: 3 }),
587
+ createLineItem({ lineItemId: '2', quantity: 8 }),
588
+ ],
589
+ });
590
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
591
+ expect(result.lineItemLevel).toBe(5);
592
+ expect(result.orderLevel).toBe(0);
593
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(3);
594
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(2);
595
+ });
596
+ it('should calculate BY_ORDER_QUANTITY with flat amount as order-level cost', () => {
597
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, { flatAmount: 0.50 });
598
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 10 }), initLineItemCosts(), 'USD', emptyRatesMap);
599
+ expect(result.orderLevel).toBe(5);
600
+ expect(result.lineItemLevel).toBe(0);
601
+ });
602
+ it('should calculate BY_ORDER_QUANTITY with tiers as order-level cost', () => {
603
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, {
604
+ quantityIntervals: [
605
+ { quantityMin: 1, quantityMax: 5, flatAmount: 3 },
606
+ { quantityMin: 6, quantityMax: null, flatAmount: 2 },
607
+ ],
608
+ });
609
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 3 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(3);
610
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 10 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(2);
611
+ });
612
+ it('should calculate BY_ORDER_QUANTITY with multiple tiers correctly', () => {
613
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, {
614
+ quantityIntervals: [
615
+ { quantityMin: 1, quantityMax: 10, flatAmount: 5 },
616
+ { quantityMin: 11, quantityMax: 50, flatAmount: 3 },
617
+ { quantityMin: 51, quantityMax: null, flatAmount: 1 },
618
+ ],
619
+ });
620
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 1 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
621
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 10 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
622
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 25 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(3);
623
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 100 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(1);
624
+ });
625
+ it('should return 0 for BY_ORDER_QUANTITY when quantity does not match any tier', () => {
626
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, {
627
+ quantityIntervals: [
628
+ { quantityMin: 5, quantityMax: 10, flatAmount: 3 },
629
+ ],
630
+ });
631
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 2 }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
632
+ });
633
+ it('should calculate BY_ORDER_QUANTITY with empty quantityIntervals using flat amount', () => {
634
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, { flatAmount: 2, quantityIntervals: [] });
635
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 7 }), initLineItemCosts(), 'USD', emptyRatesMap);
636
+ expect(result.orderLevel).toBe(14);
637
+ });
638
+ it('should return 0 for BY_LINE_ITEM_QUANTITY when line item quantity is below all tiers', () => {
639
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
640
+ quantityIntervals: [{ quantityMin: 5, quantityMax: 10, flatAmount: 3 }],
641
+ });
642
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ lineItems: [createLineItem({ quantity: 2 })] }), initLineItemCosts(), 'USD', emptyRatesMap);
643
+ expect(result.lineItemLevel).toBe(0);
644
+ expect(result.orderLevel).toBe(0);
645
+ });
646
+ it('should apply BY_LINE_ITEM_QUANTITY filters per line item, skipping non-matching ones', () => {
647
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
648
+ flatAmount: 1,
649
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
650
+ });
651
+ const liCosts = {
652
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
653
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
654
+ };
655
+ const context = createContext({
656
+ lineItems: [
657
+ createLineItem({ lineItemId: '1', quantity: 4, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } }),
658
+ createLineItem({ lineItemId: '2', quantity: 6, filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Adidas' } }),
659
+ ],
660
+ });
661
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
662
+ expect(result.lineItemLevel).toBe(4);
663
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(4);
664
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(0);
665
+ });
666
+ it('should convert flat amount for BY_ORDER_QUANTITY', () => {
667
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, { flatAmount: 5, currency: 'GBP' });
668
+ const rates = { GBP: { USD: 1.27 } };
669
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 10 }), initLineItemCosts(), 'USD', rates);
670
+ expect(result.orderLevel).toBeCloseTo(63.5);
671
+ expect(result.lineItemLevel).toBe(0);
672
+ });
673
+ it('should convert tier amount currency for BY_ORDER_QUANTITY', () => {
674
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, {
675
+ currency: 'EUR',
676
+ quantityIntervals: [{ quantityMin: 1, quantityMax: null, flatAmount: 10 }],
677
+ });
678
+ const rates = { EUR: { USD: 1.1 } };
679
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalQuantity: 5 }), initLineItemCosts(), 'USD', rates);
680
+ expect(result.orderLevel).toBeCloseTo(11);
681
+ });
682
+ it('should return 0 for BY_ORDER_QUANTITY when order filters do not match', () => {
683
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, {
684
+ flatAmount: 5,
685
+ filters: { orderTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['wholesale'] } },
686
+ });
687
+ const context = createContext({
688
+ totalQuantity: 10,
689
+ orderFilterContext: { tags: ['retail'], paymentGateways: [], shippingTitles: [] },
690
+ });
691
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
692
+ });
693
+ it('should calculate PER_REFUND_ORDER when hasRefund as order-level cost', () => {
694
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, { flatAmount: 5 });
695
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ hasRefund: true }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
696
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ hasRefund: false }), initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
697
+ });
698
+ it('should calculate PER_REFUND_LINE_ITEM as line-item-level cost', () => {
699
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, { flatAmount: 2 });
700
+ const liCosts = initLineItemCosts();
701
+ const context = createContext({
702
+ hasRefund: true,
703
+ lineItems: [createLineItem({ refundedQuantity: 2 })],
704
+ });
705
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
706
+ expect(result.orderLevel).toBe(0);
707
+ expect(result.lineItemLevel).toBe(4);
708
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(4);
709
+ });
710
+ it('should skip PER_REFUND_LINE_ITEM when refundedQuantity is 0', () => {
711
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, { flatAmount: 2 });
712
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext(), initLineItemCosts(), 'USD', emptyRatesMap);
713
+ expect(result.orderLevel).toBe(0);
714
+ expect(result.lineItemLevel).toBe(0);
715
+ });
716
+ it('should only count refunded line items for PER_REFUND_LINE_ITEM with mixed refund/non-refund', () => {
717
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, { flatAmount: 3 });
718
+ const liCosts = {
719
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
720
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
721
+ '3': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
722
+ };
723
+ const context = createContext({
724
+ hasRefund: true,
725
+ lineItems: [
726
+ createLineItem({ lineItemId: '1', refundedQuantity: 2 }),
727
+ createLineItem({ lineItemId: '2', refundedQuantity: 0 }),
728
+ createLineItem({ lineItemId: '3', refundedQuantity: 1 }),
729
+ ],
730
+ });
731
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
732
+ expect(result.lineItemLevel).toBe(9);
733
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
734
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(0);
735
+ expect(liCosts['3'].totalLineItemOtherCost).toBe(3);
736
+ });
737
+ it('should calculate PCT_COGS as order-level cost', () => {
738
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, { percentRate: 10 });
739
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalProductCost: 50 }), initLineItemCosts(), 'USD', emptyRatesMap);
740
+ expect(result.orderLevel).toBe(5);
741
+ });
742
+ it('should calculate PCT_SHIPPING as order-level cost', () => {
743
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_SHIPPING, { percentRate: 5 });
744
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalShipping: 20 }), initLineItemCosts(), 'USD', emptyRatesMap);
745
+ expect(result.orderLevel).toBe(1);
746
+ });
747
+ it('should calculate PCT_TOTAL_SALES as order-level cost', () => {
748
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: 2 });
749
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalPrice: 200 }), initLineItemCosts(), 'USD', emptyRatesMap);
750
+ expect(result.orderLevel).toBe(4);
751
+ });
752
+ it('should calculate PCT_GROSS_SALES as order-level cost', () => {
753
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, { percentRate: 3 });
754
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ grossSalePrice: 150 }), initLineItemCosts(), 'USD', emptyRatesMap);
755
+ expect(result.orderLevel).toBeCloseTo(4.5);
756
+ });
757
+ it('should calculate PCT_NET_SALES as order-level cost', () => {
758
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_NET_SALES, { percentRate: 4 });
759
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ netSalePrice: 80 }), initLineItemCosts(), 'USD', emptyRatesMap);
760
+ expect(result.orderLevel).toBe(3.2);
761
+ });
762
+ it('should return 0 for PCT_COGS when totalProductCost is 0', () => {
763
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, { percentRate: 10 });
764
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalProductCost: 0 }), initLineItemCosts(), 'USD', emptyRatesMap);
765
+ expect(result.orderLevel).toBe(0);
766
+ });
767
+ it('should return 0 for PCT_SHIPPING when totalShipping is 0', () => {
768
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_SHIPPING, { percentRate: 5 });
769
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalShipping: 0 }), initLineItemCosts(), 'USD', emptyRatesMap);
770
+ expect(result.orderLevel).toBe(0);
771
+ });
772
+ it('should return 0 for PER_ORDER when flatAmount is 0', () => {
773
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 0 });
774
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext(), initLineItemCosts(), 'USD', emptyRatesMap);
775
+ expect(result.orderLevel).toBe(0);
776
+ });
777
+ it('should return 0 for PER_LINE_ITEM when quantity is 0', () => {
778
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 2 });
779
+ const context = createContext({ lineItems: [createLineItem({ quantity: 0 })] });
780
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap);
781
+ expect(result.lineItemLevel).toBe(0);
782
+ });
783
+ it('should return 0 for unknown entry type', () => {
784
+ const entry = createEntry('UNKNOWN_TYPE', { flatAmount: 5 });
785
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext(), initLineItemCosts(), 'USD', emptyRatesMap);
786
+ expect(result.orderLevel).toBe(0);
787
+ expect(result.lineItemLevel).toBe(0);
788
+ });
789
+ it('should convert flat amount for BY_ORDER_WEIGHT (flat * weight mode)', () => {
790
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, { flatAmount: 2, currency: 'EUR' });
791
+ const rates = { EUR: { USD: 1.1 } };
792
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 500 }), initLineItemCosts(), 'USD', rates);
793
+ expect(result.orderLevel).toBeCloseTo(1100);
794
+ });
795
+ it('should convert tier amount for BY_ORDER_WEIGHT with tiers + currency', () => {
796
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
797
+ currency: 'EUR',
798
+ weightUnit: WEIGHT_UNIT.GRAMS,
799
+ weightIntervals: [
800
+ { weightMin: 0, weightMax: 500, flatAmount: 5 },
801
+ { weightMin: 501, weightMax: null, flatAmount: 10 },
802
+ ],
803
+ });
804
+ const rates = { EUR: { USD: 1.1 } };
805
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, createContext({ totalWeight: 300 }), initLineItemCosts(), 'USD', rates).orderLevel).toBeCloseTo(5.5);
806
+ });
807
+ it('should convert flat amount for BY_LINE_ITEM_QUANTITY', () => {
808
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, { flatAmount: 5, currency: 'GBP' });
809
+ const rates = { GBP: { USD: 1.27 } };
810
+ const liCosts = initLineItemCosts();
811
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext({ lineItems: [createLineItem({ quantity: 10 })] }), liCosts, 'USD', rates);
812
+ expect(result.lineItemLevel).toBeCloseTo(63.5);
813
+ expect(result.orderLevel).toBe(0);
814
+ });
815
+ it('should return 0 when order-level filters do not match', () => {
816
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
817
+ flatAmount: 5,
818
+ filters: { countries: { mode: COST_FILTER_MODE.INCLUDE, values: ['US'] } },
819
+ });
820
+ const context = createContext({ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], shipCountryCode: 'CA' } });
821
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap);
822
+ expect(result.orderLevel).toBe(0);
823
+ });
824
+ it('should convert flat amount from entry currency', () => {
825
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, currency: 'EUR' });
826
+ const rates = { EUR: { USD: 1.1 } };
827
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, createContext(), initLineItemCosts(), 'USD', rates);
828
+ expect(result.orderLevel).toBeCloseTo(11);
829
+ });
830
+ it('should return 0 for PER_ORDER when product filter does not match any line item', () => {
831
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
832
+ flatAmount: 5,
833
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P999'] } },
834
+ });
835
+ const context = createContext({
836
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V1', tags: [] } })],
837
+ });
838
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
839
+ });
840
+ it('should apply PER_ORDER when product filter matches a line item', () => {
841
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
842
+ flatAmount: 5,
843
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P100'] } },
844
+ });
845
+ const context = createContext({
846
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V1', tags: [] } })],
847
+ });
848
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
849
+ });
850
+ it('should apply PER_ORDER when at least one of multiple line items matches product filter', () => {
851
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
852
+ flatAmount: 5,
853
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
854
+ });
855
+ const context = createContext({
856
+ lineItems: [
857
+ createLineItem({ lineItemId: '1', filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } }),
858
+ createLineItem({ lineItemId: '2', filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Nike' } }),
859
+ ],
860
+ });
861
+ const liCosts = {
862
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
863
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
864
+ };
865
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap).orderLevel).toBe(5);
866
+ });
867
+ it('should return 0 for BY_ORDER_WEIGHT when productTags filter does not match any line item', () => {
868
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
869
+ flatAmount: 0.01,
870
+ filters: { productTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['fragile'] } },
871
+ });
872
+ const context = createContext({
873
+ totalWeight: 500,
874
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: ['lightweight'] } })],
875
+ });
876
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
877
+ });
878
+ it('should apply BY_ORDER_WEIGHT when productTags filter matches a line item', () => {
879
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
880
+ flatAmount: 0.01,
881
+ filters: { productTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['fragile'] } },
882
+ });
883
+ const context = createContext({
884
+ totalWeight: 500,
885
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: ['fragile'] } })],
886
+ });
887
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
888
+ });
889
+ it('should return 0 for BY_LINE_ITEM_QUANTITY when productVendors filter does not match', () => {
890
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
891
+ flatAmount: 0.5,
892
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
893
+ });
894
+ const context = createContext({
895
+ lineItems: [createLineItem({ quantity: 10, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } })],
896
+ });
897
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(0);
898
+ });
899
+ it('should apply BY_LINE_ITEM_QUANTITY when productVendors filter matches', () => {
900
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
901
+ flatAmount: 0.5,
902
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
903
+ });
904
+ const context = createContext({
905
+ lineItems: [createLineItem({ quantity: 10, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } })],
906
+ });
907
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(5);
908
+ });
909
+ it('should return 0 for PCT_GROSS_SALES when productTypes filter does not match', () => {
910
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, {
911
+ percentRate: 3,
912
+ filters: { productTypes: { mode: COST_FILTER_MODE.INCLUDE, values: ['Electronics'] } },
913
+ });
914
+ const context = createContext({
915
+ grossSalePrice: 150,
916
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], productType: 'Clothing' } })],
917
+ });
918
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
919
+ });
920
+ it('should apply PCT_GROSS_SALES when productTypes filter matches', () => {
921
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, {
922
+ percentRate: 3,
923
+ filters: { productTypes: { mode: COST_FILTER_MODE.INCLUDE, values: ['Electronics'] } },
924
+ });
925
+ const context = createContext({
926
+ grossSalePrice: 150,
927
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], productType: 'Electronics' } })],
928
+ });
929
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBeCloseTo(4.5);
930
+ });
931
+ it('should return 0 for PCT_NET_SALES when productCategories filter does not match', () => {
932
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_NET_SALES, {
933
+ percentRate: 5,
934
+ filters: { productCategories: { mode: COST_FILTER_MODE.INCLUDE, values: ['Home > Kitchen'] } },
935
+ });
936
+ const context = createContext({
937
+ netSalePrice: 80,
938
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], category: 'Sports' } })],
939
+ });
940
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
941
+ });
942
+ it('should return 0 for PCT_TOTAL_SALES when products filter does not match', () => {
943
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, {
944
+ percentRate: 2,
945
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P999'] } },
946
+ });
947
+ const context = createContext({
948
+ totalPrice: 200,
949
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V1', tags: [] } })],
950
+ });
951
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
952
+ });
953
+ it('should return 0 for PCT_COGS when product filter does not match', () => {
954
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, {
955
+ percentRate: 10,
956
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Apple'] } },
957
+ });
958
+ const context = createContext({
959
+ totalProductCost: 50,
960
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Samsung' } })],
961
+ });
962
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
963
+ });
964
+ it('should return 0 for PCT_SHIPPING when product filter does not match', () => {
965
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_SHIPPING, {
966
+ percentRate: 5,
967
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P500'] } },
968
+ });
969
+ const context = createContext({
970
+ totalShipping: 20,
971
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V1', tags: [] } })],
972
+ });
973
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
974
+ });
975
+ it('should return 0 for PER_REFUND_ORDER when product filter does not match', () => {
976
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, {
977
+ flatAmount: 5,
978
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
979
+ });
980
+ const context = createContext({
981
+ hasRefund: true,
982
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } })],
983
+ });
984
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
985
+ });
986
+ it('should apply PER_REFUND_ORDER when product filter matches and hasRefund', () => {
987
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, {
988
+ flatAmount: 5,
989
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
990
+ });
991
+ const context = createContext({
992
+ hasRefund: true,
993
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } })],
994
+ });
995
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
996
+ });
997
+ it('should only apply PER_LINE_ITEM cost to line items matching product filter', () => {
998
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
999
+ flatAmount: 2,
1000
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1001
+ });
1002
+ const liCosts = {
1003
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1004
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1005
+ };
1006
+ const context = createContext({
1007
+ lineItems: [
1008
+ createLineItem({ lineItemId: '1', quantity: 3, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } }),
1009
+ createLineItem({ lineItemId: '2', quantity: 2, filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Adidas' } }),
1010
+ ],
1011
+ });
1012
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
1013
+ expect(result.lineItemLevel).toBe(6);
1014
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
1015
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(0);
1016
+ });
1017
+ it('should return 0 for PER_LINE_ITEM when no line items match product filter', () => {
1018
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
1019
+ flatAmount: 2,
1020
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1021
+ });
1022
+ const context = createContext({
1023
+ lineItems: [
1024
+ createLineItem({ lineItemId: '1', quantity: 3, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } }),
1025
+ ],
1026
+ });
1027
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(0);
1028
+ });
1029
+ it('should only apply PER_REFUND_LINE_ITEM cost to refunded line items matching product filter', () => {
1030
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, {
1031
+ flatAmount: 3,
1032
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P1'] } },
1033
+ });
1034
+ const liCosts = {
1035
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1036
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1037
+ };
1038
+ const context = createContext({
1039
+ hasRefund: true,
1040
+ lineItems: [
1041
+ createLineItem({ lineItemId: '1', refundedQuantity: 2, filterContext: { productId: 'P1', variantId: 'V1', tags: [] } }),
1042
+ createLineItem({ lineItemId: '2', refundedQuantity: 1, filterContext: { productId: 'P2', variantId: 'V2', tags: [] } }),
1043
+ ],
1044
+ });
1045
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
1046
+ expect(result.lineItemLevel).toBe(6);
1047
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
1048
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(0);
1049
+ });
1050
+ it('should check both order and product filters for PER_ORDER (both must pass)', () => {
1051
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1052
+ flatAmount: 5,
1053
+ filters: {
1054
+ countries: { mode: COST_FILTER_MODE.INCLUDE, values: ['US'] },
1055
+ productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] },
1056
+ },
1057
+ });
1058
+ const contextPass = createContext({
1059
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], shipCountryCode: 'US' },
1060
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } })],
1061
+ });
1062
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, contextPass, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1063
+ const contextProductFail = createContext({
1064
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], shipCountryCode: 'US' },
1065
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } })],
1066
+ });
1067
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, contextProductFail, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1068
+ const contextOrderFail = createContext({
1069
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], shipCountryCode: 'CA' },
1070
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } })],
1071
+ });
1072
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, contextOrderFail, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1073
+ });
1074
+ it('should return 0 for BY_ORDER_WEIGHT when order-level country filter does not match', () => {
1075
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
1076
+ flatAmount: 0.01,
1077
+ filters: { countries: { mode: COST_FILTER_MODE.INCLUDE, values: ['US'] } },
1078
+ });
1079
+ const context = createContext({
1080
+ totalWeight: 500,
1081
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], shipCountryCode: 'CA' },
1082
+ });
1083
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1084
+ });
1085
+ it('should return 0 for BY_LINE_ITEM_QUANTITY when orderTags filter does not match', () => {
1086
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
1087
+ flatAmount: 0.5,
1088
+ filters: { orderTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['wholesale'] } },
1089
+ });
1090
+ const context = createContext({
1091
+ lineItems: [createLineItem({ quantity: 10 })],
1092
+ orderFilterContext: { tags: ['retail'], paymentGateways: [], shippingTitles: [] },
1093
+ });
1094
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(0);
1095
+ });
1096
+ it('should return 0 for PCT_GROSS_SALES when orderPaymentGateways filter does not match', () => {
1097
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, {
1098
+ percentRate: 3,
1099
+ filters: { orderPaymentGateways: { mode: COST_FILTER_MODE.INCLUDE, values: ['stripe'] } },
1100
+ });
1101
+ const context = createContext({
1102
+ grossSalePrice: 150,
1103
+ orderFilterContext: { tags: [], paymentGateways: ['paypal'], shippingTitles: [] },
1104
+ });
1105
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1106
+ });
1107
+ it('should apply PCT_GROSS_SALES when orderPaymentGateways filter matches', () => {
1108
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, {
1109
+ percentRate: 3,
1110
+ filters: { orderPaymentGateways: { mode: COST_FILTER_MODE.INCLUDE, values: ['stripe'] } },
1111
+ });
1112
+ const context = createContext({
1113
+ grossSalePrice: 150,
1114
+ orderFilterContext: { tags: [], paymentGateways: ['stripe'], shippingTitles: [] },
1115
+ });
1116
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBeCloseTo(4.5);
1117
+ });
1118
+ it('should return 0 for PER_LINE_ITEM when orderShippingTitles filter does not match', () => {
1119
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
1120
+ flatAmount: 2,
1121
+ filters: { orderShippingTitles: { mode: COST_FILTER_MODE.INCLUDE, values: ['Express'] } },
1122
+ });
1123
+ const context = createContext({
1124
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: ['Standard'] },
1125
+ lineItems: [createLineItem({ quantity: 3 })],
1126
+ });
1127
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(0);
1128
+ });
1129
+ it('should return 0 for PCT_COGS when orderSourceNames filter does not match', () => {
1130
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, {
1131
+ percentRate: 10,
1132
+ filters: { orderSourceNames: { mode: COST_FILTER_MODE.INCLUDE, values: ['web'] } },
1133
+ });
1134
+ const context = createContext({
1135
+ totalProductCost: 50,
1136
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], sourceName: 'pos' },
1137
+ });
1138
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1139
+ });
1140
+ it('should apply PER_ORDER when EXCLUDE product filter does not match any line item vendor', () => {
1141
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1142
+ flatAmount: 5,
1143
+ filters: { productVendors: { mode: COST_FILTER_MODE.EXCLUDE, values: ['Nike'] } },
1144
+ });
1145
+ const context = createContext({
1146
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } })],
1147
+ });
1148
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1149
+ });
1150
+ it('should return 0 for PER_ORDER when EXCLUDE product filter matches all line items', () => {
1151
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1152
+ flatAmount: 5,
1153
+ filters: { productVendors: { mode: COST_FILTER_MODE.EXCLUDE, values: ['Nike'] } },
1154
+ });
1155
+ const context = createContext({
1156
+ lineItems: [
1157
+ createLineItem({ lineItemId: '1', filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } }),
1158
+ ],
1159
+ });
1160
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1161
+ });
1162
+ it('should apply PER_ORDER when product filter mode is ALL', () => {
1163
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1164
+ flatAmount: 5,
1165
+ filters: { productVendors: { mode: COST_FILTER_MODE.ALL, values: [] } },
1166
+ });
1167
+ const context = createContext({
1168
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'anything' } })],
1169
+ });
1170
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1171
+ });
1172
+ it('should return 0 for PER_ORDER when multiple product filters set and no single line item satisfies all', () => {
1173
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1174
+ flatAmount: 5,
1175
+ filters: {
1176
+ productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] },
1177
+ productTypes: { mode: COST_FILTER_MODE.INCLUDE, values: ['Shoes'] },
1178
+ },
1179
+ });
1180
+ const context = createContext({
1181
+ lineItems: [
1182
+ createLineItem({ lineItemId: '1', filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike', productType: 'Hats' } }),
1183
+ createLineItem({ lineItemId: '2', filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Adidas', productType: 'Shoes' } }),
1184
+ ],
1185
+ });
1186
+ const liCosts = {
1187
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1188
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1189
+ };
1190
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap).orderLevel).toBe(0);
1191
+ });
1192
+ it('should apply PER_ORDER when one line item satisfies all product filters', () => {
1193
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1194
+ flatAmount: 5,
1195
+ filters: {
1196
+ productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] },
1197
+ productTypes: { mode: COST_FILTER_MODE.INCLUDE, values: ['Shoes'] },
1198
+ },
1199
+ });
1200
+ const context = createContext({
1201
+ lineItems: [
1202
+ createLineItem({ lineItemId: '1', filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas', productType: 'Hats' } }),
1203
+ createLineItem({ lineItemId: '2', filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Nike', productType: 'Shoes' } }),
1204
+ ],
1205
+ });
1206
+ const liCosts = {
1207
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1208
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1209
+ };
1210
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap).orderLevel).toBe(5);
1211
+ });
1212
+ it('should match PER_ORDER with productId_variantId composite filter', () => {
1213
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1214
+ flatAmount: 5,
1215
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P100_V200'] } },
1216
+ });
1217
+ const matchContext = createContext({
1218
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V200', tags: [] } })],
1219
+ });
1220
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, matchContext, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1221
+ const noMatchContext = createContext({
1222
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V999', tags: [] } })],
1223
+ });
1224
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, noMatchContext, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1225
+ });
1226
+ it('should convert currency AND evaluate product filters for PER_ORDER', () => {
1227
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1228
+ flatAmount: 10,
1229
+ currency: 'EUR',
1230
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1231
+ });
1232
+ const rates = { EUR: { USD: 1.1 } };
1233
+ const matchContext = createContext({
1234
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } })],
1235
+ });
1236
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, matchContext, initLineItemCosts(), 'USD', rates).orderLevel).toBeCloseTo(11);
1237
+ const noMatchContext = createContext({
1238
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Adidas' } })],
1239
+ });
1240
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, noMatchContext, initLineItemCosts(), 'USD', rates).orderLevel).toBe(0);
1241
+ });
1242
+ it('should evaluate weight tiers AND product filters for BY_ORDER_WEIGHT', () => {
1243
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
1244
+ weightUnit: WEIGHT_UNIT.KILOGRAMS,
1245
+ weightIntervals: [
1246
+ { weightMin: 0, weightMax: 5, flatAmount: 3 },
1247
+ { weightMin: 5.01, weightMax: null, flatAmount: 8 },
1248
+ ],
1249
+ filters: { productCategories: { mode: COST_FILTER_MODE.INCLUDE, values: ['Heavy Items'] } },
1250
+ });
1251
+ const matchContext = createContext({
1252
+ totalWeight: 2000,
1253
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], category: 'Heavy Items' } })],
1254
+ });
1255
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, matchContext, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(3);
1256
+ const noMatchContext = createContext({
1257
+ totalWeight: 2000,
1258
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], category: 'Light Items' } })],
1259
+ });
1260
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, noMatchContext, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1261
+ });
1262
+ it('should evaluate quantity tiers AND product filters for BY_LINE_ITEM_QUANTITY', () => {
1263
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
1264
+ quantityIntervals: [
1265
+ { quantityMin: 1, quantityMax: 10, flatAmount: 2 },
1266
+ { quantityMin: 11, quantityMax: null, flatAmount: 1 },
1267
+ ],
1268
+ filters: { productTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['bulk'] } },
1269
+ });
1270
+ const matchContext = createContext({
1271
+ lineItems: [createLineItem({ quantity: 5, filterContext: { productId: 'P1', variantId: 'V1', tags: ['bulk'] } })],
1272
+ });
1273
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, matchContext, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(2);
1274
+ const noMatchContext = createContext({
1275
+ lineItems: [createLineItem({ quantity: 5, filterContext: { productId: 'P1', variantId: 'V1', tags: ['single'] } })],
1276
+ });
1277
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, noMatchContext, initLineItemCosts(), 'USD', emptyRatesMap).lineItemLevel).toBe(0);
1278
+ });
1279
+ it('should apply PER_LINE_ITEM to line items NOT matching EXCLUDE product filter', () => {
1280
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
1281
+ flatAmount: 2,
1282
+ filters: { productVendors: { mode: COST_FILTER_MODE.EXCLUDE, values: ['Nike'] } },
1283
+ });
1284
+ const liCosts = {
1285
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1286
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1287
+ };
1288
+ const context = createContext({
1289
+ lineItems: [
1290
+ createLineItem({ lineItemId: '1', quantity: 3, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } }),
1291
+ createLineItem({ lineItemId: '2', quantity: 2, filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Adidas' } }),
1292
+ ],
1293
+ });
1294
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
1295
+ expect(result.lineItemLevel).toBe(4);
1296
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(0);
1297
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(4);
1298
+ });
1299
+ it('should apply PER_LINE_ITEM selectively when multiple product filters used', () => {
1300
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
1301
+ flatAmount: 1,
1302
+ filters: {
1303
+ productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] },
1304
+ productTypes: { mode: COST_FILTER_MODE.INCLUDE, values: ['Shoes'] },
1305
+ },
1306
+ });
1307
+ const liCosts = {
1308
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1309
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1310
+ '3': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1311
+ };
1312
+ const context = createContext({
1313
+ lineItems: [
1314
+ createLineItem({ lineItemId: '1', quantity: 2, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike', productType: 'Shoes' } }),
1315
+ createLineItem({ lineItemId: '2', quantity: 3, filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Nike', productType: 'Hats' } }),
1316
+ createLineItem({ lineItemId: '3', quantity: 1, filterContext: { productId: 'P3', variantId: 'V3', tags: [], vendor: 'Adidas', productType: 'Shoes' } }),
1317
+ ],
1318
+ });
1319
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
1320
+ expect(result.lineItemLevel).toBe(2);
1321
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(2);
1322
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(0);
1323
+ expect(liCosts['3'].totalLineItemOtherCost).toBe(0);
1324
+ });
1325
+ it('should return 0 for PER_REFUND_ORDER when product filter matches but hasRefund is false', () => {
1326
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, {
1327
+ flatAmount: 5,
1328
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1329
+ });
1330
+ const context = createContext({
1331
+ hasRefund: false,
1332
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } })],
1333
+ });
1334
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1335
+ });
1336
+ it('should apply PER_REFUND_LINE_ITEM only to refunded line items NOT matching EXCLUDE filter', () => {
1337
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, {
1338
+ flatAmount: 3,
1339
+ filters: { products: { mode: COST_FILTER_MODE.EXCLUDE, values: ['P1'] } },
1340
+ });
1341
+ const liCosts = {
1342
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1343
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1344
+ };
1345
+ const context = createContext({
1346
+ hasRefund: true,
1347
+ lineItems: [
1348
+ createLineItem({ lineItemId: '1', refundedQuantity: 2, filterContext: { productId: 'P1', variantId: 'V1', tags: [] } }),
1349
+ createLineItem({ lineItemId: '2', refundedQuantity: 1, filterContext: { productId: 'P2', variantId: 'V2', tags: [] } }),
1350
+ ],
1351
+ });
1352
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
1353
+ expect(result.lineItemLevel).toBe(3);
1354
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(0);
1355
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(3);
1356
+ });
1357
+ it('should apply PER_ORDER when EXCLUDE order filter does not match', () => {
1358
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1359
+ flatAmount: 5,
1360
+ filters: { orderTags: { mode: COST_FILTER_MODE.EXCLUDE, values: ['test'] } },
1361
+ });
1362
+ const context = createContext({
1363
+ orderFilterContext: { tags: ['vip'], paymentGateways: [], shippingTitles: [] },
1364
+ });
1365
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1366
+ });
1367
+ it('should return 0 for PER_ORDER when EXCLUDE order filter matches', () => {
1368
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1369
+ flatAmount: 5,
1370
+ filters: { orderTags: { mode: COST_FILTER_MODE.EXCLUDE, values: ['test'] } },
1371
+ });
1372
+ const context = createContext({
1373
+ orderFilterContext: { tags: ['test'], paymentGateways: [], shippingTitles: [] },
1374
+ });
1375
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1376
+ });
1377
+ it('should handle undefined filter context fields (missing vendor, productType, category)', () => {
1378
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1379
+ flatAmount: 5,
1380
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1381
+ });
1382
+ const context = createContext({
1383
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [] } })],
1384
+ });
1385
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1386
+ });
1387
+ it('should handle EXCLUDE filter with undefined filter context field (undefined treated as not matching)', () => {
1388
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1389
+ flatAmount: 5,
1390
+ filters: { productVendors: { mode: COST_FILTER_MODE.EXCLUDE, values: ['Nike'] } },
1391
+ });
1392
+ const context = createContext({
1393
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [] } })],
1394
+ });
1395
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1396
+ });
1397
+ it('should return 0 for PER_ORDER when INCLUDE productTags filter and line item has empty tags', () => {
1398
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1399
+ flatAmount: 5,
1400
+ filters: { productTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['sale'] } },
1401
+ });
1402
+ const context = createContext({
1403
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [] } })],
1404
+ });
1405
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(0);
1406
+ });
1407
+ it('should apply PER_ORDER when EXCLUDE productTags filter and line item has empty tags', () => {
1408
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1409
+ flatAmount: 5,
1410
+ filters: { productTags: { mode: COST_FILTER_MODE.EXCLUDE, values: ['sale'] } },
1411
+ });
1412
+ const context = createContext({
1413
+ lineItems: [createLineItem({ filterContext: { productId: 'P1', variantId: 'V1', tags: [] } })],
1414
+ });
1415
+ expect(CostCalculatorService.calculateOtherCostEntry(entry, context, initLineItemCosts(), 'USD', emptyRatesMap).orderLevel).toBe(5);
1416
+ });
1417
+ it('should apply PER_LINE_ITEM only to line items matching product filter when order filter also passes', () => {
1418
+ const entry = createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
1419
+ flatAmount: 2,
1420
+ filters: {
1421
+ countries: { mode: COST_FILTER_MODE.INCLUDE, values: ['US'] },
1422
+ productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] },
1423
+ },
1424
+ });
1425
+ const liCosts = {
1426
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1427
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1428
+ };
1429
+ const context = createContext({
1430
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [], shipCountryCode: 'US' },
1431
+ lineItems: [
1432
+ createLineItem({ lineItemId: '1', quantity: 3, filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' } }),
1433
+ createLineItem({ lineItemId: '2', quantity: 2, filterContext: { productId: 'P2', variantId: 'V2', tags: [], vendor: 'Adidas' } }),
1434
+ ],
1435
+ });
1436
+ const result = CostCalculatorService.calculateOtherCostEntry(entry, context, liCosts, 'USD', emptyRatesMap);
1437
+ expect(result.lineItemLevel).toBe(6);
1438
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
1439
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(0);
1440
+ });
1441
+ });
1442
+ describe('calculateOtherCost', () => {
1443
+ it('should return 0 when no order config', () => {
1444
+ const configs = createConfigs();
1445
+ const result = CostCalculatorService.calculateOtherCost(configs, createContext(), {}, 'USD', emptyRatesMap);
1446
+ expect(result.total).toBe(0);
1447
+ expect(result.orderLevel).toBe(0);
1448
+ expect(result.lineItemLevel).toBe(0);
1449
+ });
1450
+ it('should sum multiple entries and separate order-level costs', () => {
1451
+ const configs = createConfigs({
1452
+ order: {
1453
+ entries: [
1454
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 3 }),
1455
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 2 }),
1456
+ ],
1457
+ },
1458
+ });
1459
+ const liCosts = { '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 } };
1460
+ const result = CostCalculatorService.calculateOtherCost(configs, createContext(), liCosts, 'USD', emptyRatesMap);
1461
+ expect(result.total).toBe(5);
1462
+ expect(result.orderLevel).toBe(5);
1463
+ expect(result.lineItemLevel).toBe(0);
1464
+ });
1465
+ it('should accumulate lineItemCosts from multiple PER_LINE_ITEM entries', () => {
1466
+ const configs = createConfigs({
1467
+ order: {
1468
+ entries: [
1469
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'e1', flatAmount: 2 }),
1470
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'e2', flatAmount: 3 }),
1471
+ ],
1472
+ },
1473
+ });
1474
+ const context = createContext({
1475
+ lineItems: [createLineItem({ lineItemId: '1', quantity: 2 })],
1476
+ });
1477
+ const liCosts = {
1478
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1479
+ };
1480
+ const result = CostCalculatorService.calculateOtherCost(configs, context, liCosts, 'USD', emptyRatesMap);
1481
+ expect(result.total).toBe(10);
1482
+ expect(result.orderLevel).toBe(0);
1483
+ expect(result.lineItemLevel).toBe(10);
1484
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(10);
1485
+ });
1486
+ it('should skip entries outside date range', () => {
1487
+ const configs = createConfigs({
1488
+ order: {
1489
+ entries: [
1490
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 3, effectiveFrom: '2025-01-01T00:00:00Z' }),
1491
+ ],
1492
+ },
1493
+ });
1494
+ const liCosts = {};
1495
+ const result = CostCalculatorService.calculateOtherCost(configs, createContext(), liCosts, 'USD', emptyRatesMap);
1496
+ expect(result.total).toBe(0);
1497
+ });
1498
+ it('should only sum entries whose filters pass (mixed filter results)', () => {
1499
+ const configs = createConfigs({
1500
+ order: {
1501
+ entries: [
1502
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1503
+ id: 'e1', flatAmount: 3,
1504
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1505
+ }),
1506
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1507
+ id: 'e2', flatAmount: 7,
1508
+ filters: { products: { mode: COST_FILTER_MODE.INCLUDE, values: ['P999'] } },
1509
+ }),
1510
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, {
1511
+ id: 'e3', percentRate: 2,
1512
+ }),
1513
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1514
+ id: 'e4', flatAmount: 10,
1515
+ filters: { countries: { mode: COST_FILTER_MODE.INCLUDE, values: ['CA'] } },
1516
+ }),
1517
+ ],
1518
+ },
1519
+ });
1520
+ const context = createContext({
1521
+ grossSalePrice: 120,
1522
+ orderFilterContext: { tags: [], paymentGateways: ['stripe'], shippingTitles: ['Standard'], shipCountryCode: 'US' },
1523
+ lineItems: [createLineItem({ filterContext: { productId: 'P100', variantId: 'V1', tags: [], vendor: 'Nike' } })],
1524
+ });
1525
+ const liCosts = { '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 } };
1526
+ const result = CostCalculatorService.calculateOtherCost(configs, context, liCosts, 'USD', emptyRatesMap);
1527
+ expect(result.total).toBeCloseTo(5.4);
1528
+ expect(result.orderLevel).toBeCloseTo(5.4);
1529
+ });
1530
+ it('should sum PER_ORDER and PER_LINE_ITEM entries together and separate by level', () => {
1531
+ const configs = createConfigs({
1532
+ order: {
1533
+ entries: [
1534
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'e1', flatAmount: 5 }),
1535
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'e2', flatAmount: 2 }),
1536
+ ],
1537
+ },
1538
+ });
1539
+ const context = createContext({
1540
+ lineItems: [
1541
+ createLineItem({ lineItemId: '1', quantity: 3 }),
1542
+ createLineItem({ lineItemId: '2', quantity: 2 }),
1543
+ ],
1544
+ });
1545
+ const liCosts = {
1546
+ '1': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1547
+ '2': { totalLineItemHandlingCost: 0, totalLineItemOtherCost: 0 },
1548
+ };
1549
+ const result = CostCalculatorService.calculateOtherCost(configs, context, liCosts, 'USD', emptyRatesMap);
1550
+ expect(result.total).toBe(15);
1551
+ expect(result.orderLevel).toBe(5);
1552
+ expect(result.lineItemLevel).toBe(10);
1553
+ expect(liCosts['1'].totalLineItemOtherCost).toBe(6);
1554
+ expect(liCosts['2'].totalLineItemOtherCost).toBe(4);
1555
+ });
1556
+ });
1557
+ describe('calculateLineItemHandlingCosts', () => {
1558
+ it('should return 0 for no handling fee', () => {
1559
+ const result = CostCalculatorService.calculateLineItemHandlingCosts([createLineItem()]);
1560
+ expect(result['1']).toBe(0);
1561
+ });
1562
+ it('should multiply handling fee by quantity', () => {
1563
+ const result = CostCalculatorService.calculateLineItemHandlingCosts([
1564
+ createLineItem({ lineItemId: '1', quantity: 3, variantHandlingFee: 1.5 }),
1565
+ ]);
1566
+ expect(result['1']).toBe(4.5);
1567
+ });
1568
+ it('should handle multiple line items', () => {
1569
+ const result = CostCalculatorService.calculateLineItemHandlingCosts([
1570
+ createLineItem({ lineItemId: '1', quantity: 2, variantHandlingFee: 1 }),
1571
+ createLineItem({ lineItemId: '2', quantity: 1, variantHandlingFee: 3 }),
1572
+ ]);
1573
+ expect(result['1']).toBe(2);
1574
+ expect(result['2']).toBe(3);
1575
+ });
1576
+ });
1577
+ describe('calculateOrderCosts', () => {
1578
+ it('should return default result when configs is null', () => {
1579
+ const result = CostCalculatorService.calculateOrderCosts(null, createContext(), 'USD', emptyRatesMap);
1580
+ expect(result.orderLevelHandlingCost).toBe(0);
1581
+ expect(result.lineItemLevelHandlingCost).toBe(0);
1582
+ expect(result.totalHandlingCost).toBe(0);
1583
+ expect(result.totalOtherCost).toBe(0);
1584
+ expect(result.orderLevelOtherCost).toBe(0);
1585
+ expect(result.lineItemLevelOtherCost).toBe(0);
1586
+ expect(result.totalGatewayCost).toBe(0);
1587
+ expect(result.totalShippingCost).toBe(0);
1588
+ });
1589
+ it('should calculate all cost types together', () => {
1590
+ const configs = createConfigs({
1591
+ globalProductOverride: { globalHandlingFee: 2 },
1592
+ gateway: {
1593
+ entries: [{ gatewayName: 'stripe', flatAmount: 0.30, percentRate: 2.9, currency: 'USD', createdAt: '', updatedAt: '' }],
1594
+ },
1595
+ shipping: { method: SHIPPING_METHOD.SHOPIFY_CHARGES, currency: 'USD' },
1596
+ order: {
1597
+ entries: [createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 1 })],
1598
+ },
1599
+ });
1600
+ const context = createContext({
1601
+ totalShipping: 10,
1602
+ lineItems: [createLineItem({ variantHandlingFee: 0.5, quantity: 2 })],
1603
+ });
1604
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1605
+ expect(result.orderLevelHandlingCost).toBe(0);
1606
+ expect(result.lineItemLevelHandlingCost).toBe(3);
1607
+ expect(result.totalHandlingCost).toBe(3);
1608
+ expect(result.totalGatewayCost).toBeCloseTo(3.20);
1609
+ expect(result.totalShippingCost).toBe(10);
1610
+ expect(result.totalOtherCost).toBe(1);
1611
+ expect(result.orderLevelOtherCost).toBe(1);
1612
+ expect(result.lineItemLevelOtherCost).toBe(0);
1613
+ expect(result.lineItemCosts['1'].totalLineItemHandlingCost).toBe(3);
1614
+ });
1615
+ it('should handle partial configs', () => {
1616
+ const configs = createConfigs({
1617
+ gateway: {
1618
+ entries: [{ gatewayName: 'stripe', flatAmount: 0.30, percentRate: 0, currency: 'USD', createdAt: '', updatedAt: '' }],
1619
+ },
1620
+ });
1621
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1622
+ expect(result.orderLevelHandlingCost).toBe(0);
1623
+ expect(result.lineItemLevelHandlingCost).toBe(0);
1624
+ expect(result.totalHandlingCost).toBe(0);
1625
+ expect(result.totalGatewayCost).toBe(0.30);
1626
+ expect(result.totalShippingCost).toBe(0);
1627
+ expect(result.totalOtherCost).toBe(0);
1628
+ });
1629
+ it('should respect product filters in calculateOrderCosts integration', () => {
1630
+ const configs = createConfigs({
1631
+ order: {
1632
+ entries: [
1633
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1634
+ id: 'e1', flatAmount: 5,
1635
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Nike'] } },
1636
+ }),
1637
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1638
+ id: 'e2', flatAmount: 10,
1639
+ filters: { productVendors: { mode: COST_FILTER_MODE.INCLUDE, values: ['Puma'] } },
1640
+ }),
1641
+ ],
1642
+ },
1643
+ });
1644
+ const context = createContext({
1645
+ lineItems: [
1646
+ createLineItem({
1647
+ lineItemId: '1',
1648
+ quantity: 1,
1649
+ filterContext: { productId: 'P1', variantId: 'V1', tags: [], vendor: 'Nike' },
1650
+ }),
1651
+ ],
1652
+ });
1653
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1654
+ expect(result.totalOtherCost).toBe(5);
1655
+ expect(result.orderLevelOtherCost).toBe(5);
1656
+ });
1657
+ it('should accumulate all cost types with multiple line items and separate by level', () => {
1658
+ const configs = createConfigs({
1659
+ globalProductOverride: { globalHandlingFee: 1, currency: 'USD' },
1660
+ gateway: {
1661
+ entries: [{ gatewayName: 'stripe', flatAmount: 0.30, percentRate: 2.9, currency: 'USD', createdAt: '', updatedAt: '' }],
1662
+ },
1663
+ shipping: { method: SHIPPING_METHOD.FIXED_RATE, fixedRateAmount: 5, currency: 'USD' },
1664
+ order: {
1665
+ entries: [
1666
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'e1', flatAmount: 3 }),
1667
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'e2', flatAmount: 1 }),
1668
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { id: 'e3', percentRate: 2 }),
1669
+ ],
1670
+ },
1671
+ });
1672
+ const context = createContext({
1673
+ totalPrice: 200,
1674
+ lineItems: [
1675
+ createLineItem({ lineItemId: '1', quantity: 3, variantHandlingFee: 0.5 }),
1676
+ createLineItem({ lineItemId: '2', quantity: 2, variantHandlingFee: 1 }),
1677
+ ],
1678
+ });
1679
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1680
+ expect(result.orderLevelHandlingCost).toBe(0);
1681
+ expect(result.lineItemLevelHandlingCost).toBe(5.5);
1682
+ expect(result.totalHandlingCost).toBe(5.5);
1683
+ expect(result.totalGatewayCost).toBeCloseTo(6.10);
1684
+ expect(result.totalShippingCost).toBe(5);
1685
+ expect(result.totalOtherCost).toBe(12);
1686
+ expect(result.orderLevelOtherCost).toBe(7);
1687
+ expect(result.lineItemLevelOtherCost).toBe(5);
1688
+ expect(result.lineItemCosts['1'].totalLineItemOtherCost).toBe(3);
1689
+ expect(result.lineItemCosts['2'].totalLineItemOtherCost).toBe(2);
1690
+ expect(result.lineItemCosts['1'].totalLineItemHandlingCost).toBe(2.5);
1691
+ expect(result.lineItemCosts['2'].totalLineItemHandlingCost).toBe(3);
1692
+ });
1693
+ it('should maintain decimal precision with Big.js', () => {
1694
+ const configs = createConfigs({
1695
+ order: {
1696
+ entries: [
1697
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: 33.33 }),
1698
+ ],
1699
+ },
1700
+ });
1701
+ const context = createContext({ totalPrice: 99.99 });
1702
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1703
+ expect(result.totalOtherCost).toBeCloseTo(33.326667, 4);
1704
+ });
1705
+ });
1706
+ describe('calculateOtherCost - category routing', () => {
1707
+ it('should route COGS category costs to totalOtherCogs', () => {
1708
+ const configs = createConfigs({
1709
+ order: {
1710
+ entries: [
1711
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'cogs1', flatAmount: 10, category: 'cogs' }),
1712
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'cogs2', flatAmount: 5, category: 'cogs' }),
1713
+ ],
1714
+ },
1715
+ });
1716
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1717
+ expect(result.categoryCosts.totalOtherCogs).toBe(15);
1718
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
1719
+ expect(result.categoryCosts.otherTransactionCost).toBe(0);
1720
+ expect(result.categoryCosts.marketingCost).toBe(0);
1721
+ expect(result.categoryCosts.agencyFeesCost).toBe(0);
1722
+ expect(result.categoryCosts.opexCost).toBe(0);
1723
+ expect(result.categoryCosts.totalOtherCost).toBe(0);
1724
+ });
1725
+ it('should route FULFILLMENT category costs to otherFulfillmentCost', () => {
1726
+ const configs = createConfigs({
1727
+ order: {
1728
+ entries: [
1729
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'ful1', flatAmount: 8, category: 'fulfillment' }),
1730
+ ],
1731
+ },
1732
+ });
1733
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1734
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(8);
1735
+ expect(result.categoryCosts.totalOtherCogs).toBe(0);
1736
+ });
1737
+ it('should route TRANSACTION category costs to otherTransactionCost', () => {
1738
+ const configs = createConfigs({
1739
+ order: {
1740
+ entries: [
1741
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { id: 'tx1', percentRate: 3, category: 'transaction' }),
1742
+ ],
1743
+ },
1744
+ });
1745
+ const context = createContext({ totalPrice: 100 });
1746
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1747
+ expect(result.categoryCosts.otherTransactionCost).toBe(3);
1748
+ });
1749
+ it('should route MARKETING category costs to marketingCost', () => {
1750
+ const configs = createConfigs({
1751
+ order: {
1752
+ entries: [
1753
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'mkt1', flatAmount: 25, category: 'marketing' }),
1754
+ ],
1755
+ },
1756
+ });
1757
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1758
+ expect(result.categoryCosts.marketingCost).toBe(25);
1759
+ });
1760
+ it('should route AGENCY_FEES category costs to agencyFeesCost', () => {
1761
+ const configs = createConfigs({
1762
+ order: {
1763
+ entries: [
1764
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, { id: 'agency1', percentRate: 15, category: 'agency_fees' }),
1765
+ ],
1766
+ },
1767
+ });
1768
+ const context = createContext({ grossSalePrice: 200 });
1769
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1770
+ expect(result.categoryCosts.agencyFeesCost).toBe(30);
1771
+ });
1772
+ it('should route OPEX category costs to opexCost', () => {
1773
+ const configs = createConfigs({
1774
+ order: {
1775
+ entries: [
1776
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'opex1', flatAmount: 12, category: 'opex' }),
1777
+ ],
1778
+ },
1779
+ });
1780
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1781
+ expect(result.categoryCosts.opexCost).toBe(12);
1782
+ });
1783
+ it('should route OTHER category costs to totalOtherCost', () => {
1784
+ const configs = createConfigs({
1785
+ order: {
1786
+ entries: [
1787
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'other1', flatAmount: 7, category: 'other' }),
1788
+ ],
1789
+ },
1790
+ });
1791
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1792
+ expect(result.categoryCosts.totalOtherCost).toBe(7);
1793
+ });
1794
+ it('should route legacy payment_fees to TRANSACTION (otherTransactionCost)', () => {
1795
+ const configs = createConfigs({
1796
+ order: {
1797
+ entries: [
1798
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'legacy1', flatAmount: 2.5, category: 'payment_fees' }),
1799
+ ],
1800
+ },
1801
+ });
1802
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1803
+ expect(result.categoryCosts.otherTransactionCost).toBe(2.5);
1804
+ expect(result.categoryCosts.totalOtherCost).toBe(0);
1805
+ });
1806
+ it('should route legacy packaging to FULFILLMENT (otherFulfillmentCost)', () => {
1807
+ const configs = createConfigs({
1808
+ order: {
1809
+ entries: [
1810
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'legacy2', flatAmount: 1.75, category: 'packaging' }),
1811
+ ],
1812
+ },
1813
+ });
1814
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1815
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(1.75);
1816
+ expect(result.categoryCosts.totalOtherCost).toBe(0);
1817
+ });
1818
+ it('should route unknown category to totalOtherCost', () => {
1819
+ const configs = createConfigs({
1820
+ order: {
1821
+ entries: [
1822
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'unknown1', flatAmount: 3, category: 'unknown_category_xyz' }),
1823
+ ],
1824
+ },
1825
+ });
1826
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1827
+ expect(result.categoryCosts.totalOtherCost).toBe(3);
1828
+ });
1829
+ it('should route undefined/null category to totalOtherCost', () => {
1830
+ const configs = createConfigs({
1831
+ order: {
1832
+ entries: [
1833
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'nocat1', flatAmount: 4, category: undefined }),
1834
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'nocat2', flatAmount: 6, category: null }),
1835
+ ],
1836
+ },
1837
+ });
1838
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1839
+ expect(result.categoryCosts.totalOtherCost).toBe(10);
1840
+ });
1841
+ it('should accumulate costs across multiple entries of the same category', () => {
1842
+ const configs = createConfigs({
1843
+ order: {
1844
+ entries: [
1845
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'ful1', flatAmount: 5, category: 'fulfillment' }),
1846
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'ful2', flatAmount: 3, category: 'fulfillment' }),
1847
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'ful3', flatAmount: 2, category: 'fulfillment' }),
1848
+ ],
1849
+ },
1850
+ });
1851
+ const context = createContext({
1852
+ lineItems: [
1853
+ createLineItem({ lineItemId: '1', quantity: 2 }),
1854
+ createLineItem({ lineItemId: '2', quantity: 3 }),
1855
+ ],
1856
+ });
1857
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1858
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(18);
1859
+ });
1860
+ it('should route costs to multiple categories correctly', () => {
1861
+ const configs = createConfigs({
1862
+ order: {
1863
+ entries: [
1864
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'cogs1', flatAmount: 10, category: 'cogs' }),
1865
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'ful1', flatAmount: 5, category: 'fulfillment' }),
1866
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { id: 'tx1', percentRate: 2, category: 'transaction' }),
1867
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'mkt1', flatAmount: 15, category: 'marketing' }),
1868
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'agency1', flatAmount: 8, category: 'agency_fees' }),
1869
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'opex1', flatAmount: 20, category: 'opex' }),
1870
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'other1', flatAmount: 3, category: 'other' }),
1871
+ ],
1872
+ },
1873
+ });
1874
+ const context = createContext({ totalPrice: 100 });
1875
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1876
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
1877
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(5);
1878
+ expect(result.categoryCosts.otherTransactionCost).toBe(2);
1879
+ expect(result.categoryCosts.marketingCost).toBe(15);
1880
+ expect(result.categoryCosts.agencyFeesCost).toBe(8);
1881
+ expect(result.categoryCosts.opexCost).toBe(20);
1882
+ expect(result.categoryCosts.totalOtherCost).toBe(3);
1883
+ expect(result.totalOtherCost).toBe(3);
1884
+ });
1885
+ it('should handle line-item level costs in category routing', () => {
1886
+ const configs = createConfigs({
1887
+ order: {
1888
+ entries: [
1889
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'cogs_li', flatAmount: 2, category: 'cogs' }),
1890
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'ful_li', flatAmount: 1.5, category: 'fulfillment' }),
1891
+ ],
1892
+ },
1893
+ });
1894
+ const context = createContext({
1895
+ lineItems: [
1896
+ createLineItem({ lineItemId: '1', quantity: 3 }),
1897
+ createLineItem({ lineItemId: '2', quantity: 2 }),
1898
+ ],
1899
+ });
1900
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1901
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
1902
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(7.5);
1903
+ });
1904
+ it('should handle PCT_COGS entry type with category routing', () => {
1905
+ const configs = createConfigs({
1906
+ order: {
1907
+ entries: [
1908
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, { id: 'pct_cogs', percentRate: 10, category: 'cogs' }),
1909
+ ],
1910
+ },
1911
+ });
1912
+ const context = createContext({ totalProductCost: 50 });
1913
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1914
+ expect(result.categoryCosts.totalOtherCogs).toBe(5);
1915
+ });
1916
+ it('should handle PCT_SHIPPING entry type routed to fulfillment', () => {
1917
+ const configs = createConfigs({
1918
+ order: {
1919
+ entries: [
1920
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_SHIPPING, { id: 'pct_ship', percentRate: 20, category: 'fulfillment' }),
1921
+ ],
1922
+ },
1923
+ });
1924
+ const context = createContext({ totalShipping: 25 });
1925
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1926
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(5);
1927
+ });
1928
+ it('should handle refund costs with category routing', () => {
1929
+ const configs = createConfigs({
1930
+ order: {
1931
+ entries: [
1932
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, { id: 'refund1', flatAmount: 15, category: 'opex' }),
1933
+ ],
1934
+ },
1935
+ });
1936
+ const context = createContext({ hasRefund: true });
1937
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1938
+ expect(result.categoryCosts.opexCost).toBe(15);
1939
+ });
1940
+ it('should not apply refund costs when hasRefund is false', () => {
1941
+ const configs = createConfigs({
1942
+ order: {
1943
+ entries: [
1944
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, { id: 'refund1', flatAmount: 15, category: 'opex' }),
1945
+ ],
1946
+ },
1947
+ });
1948
+ const context = createContext({ hasRefund: false });
1949
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1950
+ expect(result.categoryCosts.opexCost).toBe(0);
1951
+ });
1952
+ it('should respect date ranges in category cost routing', () => {
1953
+ const configs = createConfigs({
1954
+ order: {
1955
+ entries: [
1956
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1957
+ id: 'valid',
1958
+ flatAmount: 10,
1959
+ category: 'marketing',
1960
+ effectiveFrom: '2024-01-01T00:00:00Z',
1961
+ effectiveTo: '2024-12-31T23:59:59Z',
1962
+ }),
1963
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
1964
+ id: 'expired',
1965
+ flatAmount: 20,
1966
+ category: 'marketing',
1967
+ effectiveFrom: '2023-01-01T00:00:00Z',
1968
+ effectiveTo: '2023-12-31T23:59:59Z',
1969
+ }),
1970
+ ],
1971
+ },
1972
+ });
1973
+ const context = createContext({ orderCreatedAt: '2024-06-15T10:00:00Z' });
1974
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
1975
+ expect(result.categoryCosts.marketingCost).toBe(10);
1976
+ });
1977
+ it('should return default categoryCosts when configs is null', () => {
1978
+ const result = CostCalculatorService.calculateOrderCosts(null, createContext(), 'USD', emptyRatesMap);
1979
+ expect(result.categoryCosts).toEqual({
1980
+ totalOtherCogs: 0,
1981
+ otherFulfillmentCost: 0,
1982
+ otherTransactionCost: 0,
1983
+ marketingCost: 0,
1984
+ agencyFeesCost: 0,
1985
+ opexCost: 0,
1986
+ totalOtherCost: 0,
1987
+ });
1988
+ });
1989
+ it('should return default categoryCosts when order config is null', () => {
1990
+ const configs = createConfigs({ order: null });
1991
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
1992
+ expect(result.categoryCosts).toEqual({
1993
+ totalOtherCogs: 0,
1994
+ otherFulfillmentCost: 0,
1995
+ otherTransactionCost: 0,
1996
+ marketingCost: 0,
1997
+ agencyFeesCost: 0,
1998
+ opexCost: 0,
1999
+ totalOtherCost: 0,
2000
+ });
2001
+ });
2002
+ it('should handle mixed legacy and new categories', () => {
2003
+ const configs = createConfigs({
2004
+ order: {
2005
+ entries: [
2006
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'new_tx', flatAmount: 5, category: 'transaction' }),
2007
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'legacy_pf', flatAmount: 3, category: 'payment_fees' }),
2008
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'new_ful', flatAmount: 4, category: 'fulfillment' }),
2009
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'legacy_pkg', flatAmount: 2, category: 'packaging' }),
2010
+ ],
2011
+ },
2012
+ });
2013
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2014
+ expect(result.categoryCosts.otherTransactionCost).toBe(8);
2015
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(6);
2016
+ });
2017
+ it('should handle weight-based costs with category routing', () => {
2018
+ const configs = createConfigs({
2019
+ order: {
2020
+ entries: [
2021
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2022
+ id: 'weight1',
2023
+ flatAmount: 0.01,
2024
+ weightUnit: WEIGHT_UNIT.GRAMS,
2025
+ category: 'fulfillment',
2026
+ }),
2027
+ ],
2028
+ },
2029
+ });
2030
+ const context = createContext({ totalWeight: 1000 });
2031
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2032
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(10);
2033
+ });
2034
+ it('should handle quantity-based costs with category routing', () => {
2035
+ const configs = createConfigs({
2036
+ order: {
2037
+ entries: [
2038
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
2039
+ id: 'qty1',
2040
+ flatAmount: 0.5,
2041
+ category: 'fulfillment',
2042
+ }),
2043
+ ],
2044
+ },
2045
+ });
2046
+ const context = createContext({ lineItems: [createLineItem({ quantity: 10 })] });
2047
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2048
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(5);
2049
+ });
2050
+ it('should handle BY_ORDER_QUANTITY costs with category routing', () => {
2051
+ const configs = createConfigs({
2052
+ order: {
2053
+ entries: [
2054
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_QUANTITY, {
2055
+ id: 'qty-order',
2056
+ quantityIntervals: [
2057
+ { quantityMin: 1, quantityMax: 5, flatAmount: 10 },
2058
+ { quantityMin: 6, quantityMax: null, flatAmount: 5 },
2059
+ ],
2060
+ category: 'cogs',
2061
+ }),
2062
+ ],
2063
+ },
2064
+ });
2065
+ const context = createContext({
2066
+ lineItems: [
2067
+ createLineItem({ lineItemId: '1', quantity: 4 }),
2068
+ createLineItem({ lineItemId: '2', quantity: 3 }),
2069
+ ],
2070
+ totalQuantity: 7,
2071
+ });
2072
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2073
+ expect(result.categoryCosts.totalOtherCogs).toBe(5);
2074
+ expect(result.orderLevelOtherCost).toBe(5);
2075
+ expect(result.lineItemLevelOtherCost).toBe(0);
2076
+ });
2077
+ it('should continue processing other entries when one entry fails', () => {
2078
+ const configs = createConfigs({
2079
+ order: {
2080
+ entries: [
2081
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'good1', flatAmount: 10, category: 'cogs' }),
2082
+ {
2083
+ ...createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT),
2084
+ id: 'problematic',
2085
+ weightIntervals: [{ weightMin: NaN, weightMax: null, flatAmount: 5 }],
2086
+ category: 'fulfillment',
2087
+ },
2088
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'good2', flatAmount: 5, category: 'marketing' }),
2089
+ ],
2090
+ },
2091
+ });
2092
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2093
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2094
+ expect(result.categoryCosts.marketingCost).toBe(5);
2095
+ });
2096
+ });
2097
+ describe('calculateOrderCosts - category costs integration', () => {
2098
+ it('should calculate all cost types with category routing in full integration', () => {
2099
+ const configs = createConfigs({
2100
+ globalProductOverride: { globalHandlingFee: 1.5, currency: 'USD' },
2101
+ gateway: {
2102
+ entries: [{ gatewayName: 'stripe', flatAmount: 0.30, percentRate: 2.9, currency: 'USD', createdAt: '', updatedAt: '' }],
2103
+ },
2104
+ shipping: { method: SHIPPING_METHOD.FIXED_RATE, fixedRateAmount: 7, currency: 'USD' },
2105
+ order: {
2106
+ entries: [
2107
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'cogs1', flatAmount: 5, category: 'cogs' }),
2108
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'ful1', flatAmount: 3, category: 'fulfillment' }),
2109
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { id: 'tx1', percentRate: 1, category: 'transaction' }),
2110
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'mkt1', flatAmount: 2, category: 'marketing' }),
2111
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'agency1', flatAmount: 10, category: 'agency_fees' }),
2112
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'opex1', flatAmount: 8, category: 'opex' }),
2113
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'other1', flatAmount: 4, category: 'other' }),
2114
+ ],
2115
+ },
2116
+ });
2117
+ const context = createContext({
2118
+ totalPrice: 200,
2119
+ lineItems: [
2120
+ createLineItem({ lineItemId: '1', quantity: 2, variantHandlingFee: 0.5 }),
2121
+ createLineItem({ lineItemId: '2', quantity: 3, variantHandlingFee: 0.75 }),
2122
+ ],
2123
+ });
2124
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2125
+ expect(result.lineItemLevelHandlingCost).toBe(6.25);
2126
+ expect(result.totalHandlingCost).toBe(6.25);
2127
+ expect(result.totalGatewayCost).toBeCloseTo(6.10);
2128
+ expect(result.totalShippingCost).toBe(7);
2129
+ expect(result.categoryCosts.totalOtherCogs).toBe(5);
2130
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(3);
2131
+ expect(result.categoryCosts.otherTransactionCost).toBe(2);
2132
+ expect(result.categoryCosts.marketingCost).toBe(10);
2133
+ expect(result.categoryCosts.agencyFeesCost).toBe(10);
2134
+ expect(result.categoryCosts.opexCost).toBe(8);
2135
+ expect(result.categoryCosts.totalOtherCost).toBe(4);
2136
+ expect(result.totalOtherCost).toBe(4);
2137
+ });
2138
+ it('should properly separate order-level and line-item-level costs with categories', () => {
2139
+ const configs = createConfigs({
2140
+ order: {
2141
+ entries: [
2142
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'order_cogs', flatAmount: 10, category: 'cogs' }),
2143
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'li_cogs', flatAmount: 2, category: 'cogs' }),
2144
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'order_ful', flatAmount: 5, category: 'fulfillment' }),
2145
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { id: 'li_ful', flatAmount: 1, category: 'fulfillment' }),
2146
+ ],
2147
+ },
2148
+ });
2149
+ const context = createContext({
2150
+ lineItems: [
2151
+ createLineItem({ lineItemId: '1', quantity: 3 }),
2152
+ createLineItem({ lineItemId: '2', quantity: 2 }),
2153
+ ],
2154
+ });
2155
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2156
+ expect(result.orderLevelOtherCost).toBe(15);
2157
+ expect(result.lineItemLevelOtherCost).toBe(15);
2158
+ expect(result.categoryCosts.totalOtherCogs).toBe(20);
2159
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(10);
2160
+ expect(result.lineItemCosts['1'].totalLineItemOtherCost).toBe(9);
2161
+ expect(result.lineItemCosts['2'].totalLineItemOtherCost).toBe(6);
2162
+ });
2163
+ });
2164
+ describe('defensive coding - malformed input handling', () => {
2165
+ describe('empty and missing entries', () => {
2166
+ it('should handle empty entries array', () => {
2167
+ const configs = createConfigs({
2168
+ order: { entries: [] },
2169
+ });
2170
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2171
+ expect(result.categoryCosts.totalOtherCogs).toBe(0);
2172
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
2173
+ expect(result.categoryCosts.totalOtherCost).toBe(0);
2174
+ expect(result.totalOtherCost).toBe(0);
2175
+ });
2176
+ it('should handle undefined entries', () => {
2177
+ const configs = createConfigs({
2178
+ order: { entries: undefined },
2179
+ });
2180
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2181
+ expect(result.categoryCosts).toEqual({
2182
+ totalOtherCogs: 0,
2183
+ otherFulfillmentCost: 0,
2184
+ otherTransactionCost: 0,
2185
+ marketingCost: 0,
2186
+ agencyFeesCost: 0,
2187
+ opexCost: 0,
2188
+ totalOtherCost: 0,
2189
+ });
2190
+ });
2191
+ it('should handle null entries', () => {
2192
+ const configs = createConfigs({
2193
+ order: { entries: null },
2194
+ });
2195
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2196
+ expect(result.categoryCosts).toEqual({
2197
+ totalOtherCogs: 0,
2198
+ otherFulfillmentCost: 0,
2199
+ otherTransactionCost: 0,
2200
+ marketingCost: 0,
2201
+ agencyFeesCost: 0,
2202
+ opexCost: 0,
2203
+ totalOtherCost: 0,
2204
+ });
2205
+ });
2206
+ });
2207
+ describe('malformed entry data', () => {
2208
+ it('should handle entry with undefined flatAmount', () => {
2209
+ const configs = createConfigs({
2210
+ order: {
2211
+ entries: [
2212
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: undefined, category: 'cogs' }),
2213
+ ],
2214
+ },
2215
+ });
2216
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2217
+ expect(result.categoryCosts.totalOtherCogs).toBe(0);
2218
+ });
2219
+ it('should handle entry with null flatAmount', () => {
2220
+ const configs = createConfigs({
2221
+ order: {
2222
+ entries: [
2223
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: null, category: 'cogs' }),
2224
+ ],
2225
+ },
2226
+ });
2227
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2228
+ expect(result.categoryCosts.totalOtherCogs).toBe(0);
2229
+ });
2230
+ it('should handle entry with NaN flatAmount', () => {
2231
+ const configs = createConfigs({
2232
+ order: {
2233
+ entries: [
2234
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: NaN, category: 'fulfillment' }),
2235
+ ],
2236
+ },
2237
+ });
2238
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2239
+ expect(typeof result.categoryCosts.otherFulfillmentCost).toBe('number');
2240
+ });
2241
+ it('should handle entry with negative flatAmount', () => {
2242
+ const configs = createConfigs({
2243
+ order: {
2244
+ entries: [
2245
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: -10, category: 'cogs' }),
2246
+ ],
2247
+ },
2248
+ });
2249
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2250
+ expect(result.categoryCosts.totalOtherCogs).toBe(-10);
2251
+ });
2252
+ it('should handle entry with Infinity flatAmount', () => {
2253
+ const configs = createConfigs({
2254
+ order: {
2255
+ entries: [
2256
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: Infinity, category: 'marketing' }),
2257
+ ],
2258
+ },
2259
+ });
2260
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2261
+ expect(typeof result.categoryCosts.marketingCost).toBe('number');
2262
+ });
2263
+ it('should handle entry with string flatAmount', () => {
2264
+ const configs = createConfigs({
2265
+ order: {
2266
+ entries: [
2267
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: '10.50', category: 'opex' }),
2268
+ ],
2269
+ },
2270
+ });
2271
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2272
+ expect(result.categoryCosts.opexCost).toBe(10.5);
2273
+ });
2274
+ });
2275
+ describe('malformed percent rates', () => {
2276
+ it('should handle undefined percentRate', () => {
2277
+ const configs = createConfigs({
2278
+ order: {
2279
+ entries: [
2280
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: undefined, category: 'transaction' }),
2281
+ ],
2282
+ },
2283
+ });
2284
+ const context = createContext({ totalPrice: 100 });
2285
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2286
+ expect(result.categoryCosts.otherTransactionCost).toBe(0);
2287
+ });
2288
+ it('should handle null percentRate', () => {
2289
+ const configs = createConfigs({
2290
+ order: {
2291
+ entries: [
2292
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, { percentRate: null, category: 'agency_fees' }),
2293
+ ],
2294
+ },
2295
+ });
2296
+ const context = createContext({ grossSalePrice: 200 });
2297
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2298
+ expect(result.categoryCosts.agencyFeesCost).toBe(0);
2299
+ });
2300
+ it('should handle negative percentRate', () => {
2301
+ const configs = createConfigs({
2302
+ order: {
2303
+ entries: [
2304
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_NET_SALES, { percentRate: -5, category: 'marketing' }),
2305
+ ],
2306
+ },
2307
+ });
2308
+ const context = createContext({ netSalePrice: 100 });
2309
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2310
+ expect(result.categoryCosts.marketingCost).toBe(-5);
2311
+ });
2312
+ it('should handle percentRate over 100', () => {
2313
+ const configs = createConfigs({
2314
+ order: {
2315
+ entries: [
2316
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, { percentRate: 150, category: 'fulfillment' }),
2317
+ ],
2318
+ },
2319
+ });
2320
+ const context = createContext({ totalProductCost: 50 });
2321
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2322
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(75);
2323
+ });
2324
+ });
2325
+ describe('malformed context data', () => {
2326
+ it('should handle zero totalPrice for percent calculations', () => {
2327
+ const configs = createConfigs({
2328
+ order: {
2329
+ entries: [
2330
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: 10, category: 'transaction' }),
2331
+ ],
2332
+ },
2333
+ });
2334
+ const context = createContext({ totalPrice: 0 });
2335
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2336
+ expect(result.categoryCosts.otherTransactionCost).toBe(0);
2337
+ });
2338
+ it('should handle negative totalPrice', () => {
2339
+ const configs = createConfigs({
2340
+ order: {
2341
+ entries: [
2342
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: 10, category: 'cogs' }),
2343
+ ],
2344
+ },
2345
+ });
2346
+ const context = createContext({ totalPrice: -100 });
2347
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2348
+ expect(result.categoryCosts.totalOtherCogs).toBe(-10);
2349
+ });
2350
+ it('should handle empty lineItems array', () => {
2351
+ const configs = createConfigs({
2352
+ order: {
2353
+ entries: [
2354
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 5, category: 'fulfillment' }),
2355
+ ],
2356
+ },
2357
+ });
2358
+ const context = createContext({ lineItems: [] });
2359
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2360
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
2361
+ expect(result.lineItemCosts).toEqual({});
2362
+ });
2363
+ it('should handle lineItems with zero quantity', () => {
2364
+ const configs = createConfigs({
2365
+ order: {
2366
+ entries: [
2367
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 5, category: 'marketing' }),
2368
+ ],
2369
+ },
2370
+ });
2371
+ const context = createContext({
2372
+ lineItems: [createLineItem({ quantity: 0 })],
2373
+ });
2374
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2375
+ expect(result.categoryCosts.marketingCost).toBe(0);
2376
+ });
2377
+ it('should handle undefined orderCreatedAt', () => {
2378
+ const configs = createConfigs({
2379
+ order: {
2380
+ entries: [
2381
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2382
+ flatAmount: 10,
2383
+ category: 'cogs',
2384
+ effectiveFrom: '2024-01-01T00:00:00Z',
2385
+ effectiveTo: '2024-12-31T23:59:59Z',
2386
+ }),
2387
+ ],
2388
+ },
2389
+ });
2390
+ const context = createContext({ orderCreatedAt: undefined });
2391
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2392
+ expect(typeof result.categoryCosts.totalOtherCogs).toBe('number');
2393
+ });
2394
+ it('should handle invalid orderCreatedAt format', () => {
2395
+ const configs = createConfigs({
2396
+ order: {
2397
+ entries: [
2398
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2399
+ flatAmount: 10,
2400
+ category: 'opex',
2401
+ effectiveFrom: '2024-01-01T00:00:00Z',
2402
+ }),
2403
+ ],
2404
+ },
2405
+ });
2406
+ const context = createContext({ orderCreatedAt: 'not-a-date' });
2407
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2408
+ expect(result.categoryCosts.opexCost).toBe(10);
2409
+ });
2410
+ });
2411
+ describe('currency edge cases', () => {
2412
+ it('should handle empty currency string', () => {
2413
+ const configs = createConfigs({
2414
+ order: {
2415
+ entries: [
2416
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, currency: '', category: 'cogs' }),
2417
+ ],
2418
+ },
2419
+ });
2420
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2421
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2422
+ });
2423
+ it('should handle null order currency', () => {
2424
+ const configs = createConfigs({
2425
+ order: {
2426
+ entries: [
2427
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'fulfillment' }),
2428
+ ],
2429
+ },
2430
+ });
2431
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), null, emptyRatesMap);
2432
+ expect(typeof result.categoryCosts.otherFulfillmentCost).toBe('number');
2433
+ });
2434
+ it('should handle missing exchange rates', () => {
2435
+ const configs = createConfigs({
2436
+ order: {
2437
+ entries: [
2438
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, currency: 'EUR', category: 'transaction' }),
2439
+ ],
2440
+ },
2441
+ });
2442
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', {});
2443
+ expect(typeof result.categoryCosts.otherTransactionCost).toBe('number');
2444
+ });
2445
+ });
2446
+ describe('weight and quantity tier edge cases', () => {
2447
+ it('should fall back to flat mode when weightIntervals array is empty', () => {
2448
+ const configs = createConfigs({
2449
+ order: {
2450
+ entries: [
2451
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2452
+ weightIntervals: [],
2453
+ flatAmount: 5,
2454
+ category: 'fulfillment',
2455
+ }),
2456
+ ],
2457
+ },
2458
+ });
2459
+ const context = createContext({ totalWeight: 1000 });
2460
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2461
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(5000);
2462
+ });
2463
+ it('should fall back to flat mode when quantityIntervals array is empty', () => {
2464
+ const configs = createConfigs({
2465
+ order: {
2466
+ entries: [
2467
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
2468
+ quantityIntervals: [],
2469
+ flatAmount: 5,
2470
+ category: 'cogs',
2471
+ }),
2472
+ ],
2473
+ },
2474
+ });
2475
+ const context = createContext({ lineItems: [createLineItem({ quantity: 10 })] });
2476
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2477
+ expect(result.categoryCosts.totalOtherCogs).toBe(50);
2478
+ });
2479
+ it('should handle weight outside all tier ranges', () => {
2480
+ const configs = createConfigs({
2481
+ order: {
2482
+ entries: [
2483
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2484
+ weightIntervals: [
2485
+ { weightMin: 0, weightMax: 100, flatAmount: 5 },
2486
+ { weightMin: 101, weightMax: 500, flatAmount: 10 },
2487
+ ],
2488
+ weightUnit: WEIGHT_UNIT.GRAMS,
2489
+ category: 'fulfillment',
2490
+ }),
2491
+ ],
2492
+ },
2493
+ });
2494
+ const context = createContext({ totalWeight: 1000 });
2495
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2496
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
2497
+ });
2498
+ it('should handle zero totalWeight', () => {
2499
+ const configs = createConfigs({
2500
+ order: {
2501
+ entries: [
2502
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2503
+ flatAmount: 0.01,
2504
+ weightUnit: WEIGHT_UNIT.GRAMS,
2505
+ category: 'marketing',
2506
+ }),
2507
+ ],
2508
+ },
2509
+ });
2510
+ const context = createContext({ totalWeight: 0 });
2511
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2512
+ expect(result.categoryCosts.marketingCost).toBe(0);
2513
+ });
2514
+ it('should handle negative totalWeight', () => {
2515
+ const configs = createConfigs({
2516
+ order: {
2517
+ entries: [
2518
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2519
+ flatAmount: 0.01,
2520
+ weightUnit: WEIGHT_UNIT.GRAMS,
2521
+ category: 'opex',
2522
+ }),
2523
+ ],
2524
+ },
2525
+ });
2526
+ const context = createContext({ totalWeight: -100 });
2527
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2528
+ expect(typeof result.categoryCosts.opexCost).toBe('number');
2529
+ });
2530
+ });
2531
+ describe('filter context edge cases', () => {
2532
+ it('should handle undefined orderFilterContext', () => {
2533
+ const configs = createConfigs({
2534
+ order: {
2535
+ entries: [
2536
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'cogs' }),
2537
+ ],
2538
+ },
2539
+ });
2540
+ const context = createContext({ orderFilterContext: undefined });
2541
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2542
+ expect(typeof result.categoryCosts.totalOtherCogs).toBe('number');
2543
+ });
2544
+ it('should handle empty tags array in filter context', () => {
2545
+ const configs = createConfigs({
2546
+ order: {
2547
+ entries: [
2548
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2549
+ flatAmount: 10,
2550
+ category: 'fulfillment',
2551
+ filters: {
2552
+ orderTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['VIP'] },
2553
+ },
2554
+ }),
2555
+ ],
2556
+ },
2557
+ });
2558
+ const context = createContext({
2559
+ orderFilterContext: { tags: [], paymentGateways: [], shippingTitles: [] },
2560
+ });
2561
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2562
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
2563
+ });
2564
+ });
2565
+ describe('date range edge cases', () => {
2566
+ it('should handle entry with effectiveFrom after effectiveTo', () => {
2567
+ const configs = createConfigs({
2568
+ order: {
2569
+ entries: [
2570
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2571
+ flatAmount: 10,
2572
+ category: 'transaction',
2573
+ effectiveFrom: '2025-01-01T00:00:00Z',
2574
+ effectiveTo: '2024-01-01T00:00:00Z',
2575
+ }),
2576
+ ],
2577
+ },
2578
+ });
2579
+ const context = createContext({ orderCreatedAt: '2024-06-15T10:00:00Z' });
2580
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2581
+ expect(result.categoryCosts.otherTransactionCost).toBe(0);
2582
+ });
2583
+ it('should handle entry with same effectiveFrom and effectiveTo', () => {
2584
+ const configs = createConfigs({
2585
+ order: {
2586
+ entries: [
2587
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2588
+ flatAmount: 10,
2589
+ category: 'agency_fees',
2590
+ effectiveFrom: '2024-06-15T10:00:00Z',
2591
+ effectiveTo: '2024-06-15T10:00:00Z',
2592
+ }),
2593
+ ],
2594
+ },
2595
+ });
2596
+ const context = createContext({ orderCreatedAt: '2024-06-15T10:00:00Z' });
2597
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2598
+ expect(result.categoryCosts.agencyFeesCost).toBe(10);
2599
+ });
2600
+ it('should handle invalid effectiveFrom format', () => {
2601
+ const configs = createConfigs({
2602
+ order: {
2603
+ entries: [
2604
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2605
+ flatAmount: 10,
2606
+ category: 'opex',
2607
+ effectiveFrom: 'invalid-date',
2608
+ }),
2609
+ ],
2610
+ },
2611
+ });
2612
+ const context = createContext({ orderCreatedAt: '2024-06-15T10:00:00Z' });
2613
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2614
+ expect(result.categoryCosts.opexCost).toBe(10);
2615
+ });
2616
+ });
2617
+ describe('concurrent category accumulation', () => {
2618
+ it('should correctly accumulate when multiple entries have same ID but different categories', () => {
2619
+ const configs = createConfigs({
2620
+ order: {
2621
+ entries: [
2622
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'same-id', flatAmount: 10, category: 'cogs' }),
2623
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'same-id', flatAmount: 5, category: 'fulfillment' }),
2624
+ ],
2625
+ },
2626
+ });
2627
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2628
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2629
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(5);
2630
+ });
2631
+ it('should handle maximum number of entries without performance degradation', () => {
2632
+ const entries = Array.from({ length: 100 }, (_, i) => createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2633
+ id: `entry-${i}`,
2634
+ flatAmount: 1,
2635
+ category: ['cogs', 'fulfillment', 'transaction', 'marketing', 'agency_fees', 'opex', 'other'][i % 7],
2636
+ }));
2637
+ const configs = createConfigs({
2638
+ order: { entries },
2639
+ });
2640
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2641
+ const totalCost = result.categoryCosts.totalOtherCogs +
2642
+ result.categoryCosts.otherFulfillmentCost +
2643
+ result.categoryCosts.otherTransactionCost +
2644
+ result.categoryCosts.marketingCost +
2645
+ result.categoryCosts.agencyFeesCost +
2646
+ result.categoryCosts.opexCost +
2647
+ result.categoryCosts.totalOtherCost;
2648
+ expect(totalCost).toBe(100);
2649
+ });
2650
+ });
2651
+ });
2652
+ describe('Accounting Identity Tests', () => {
2653
+ describe('sum of category costs equals order + line-item level costs', () => {
2654
+ const sumCategoryCosts = (categoryCosts) => {
2655
+ return (categoryCosts.totalOtherCogs +
2656
+ categoryCosts.otherFulfillmentCost +
2657
+ categoryCosts.otherTransactionCost +
2658
+ categoryCosts.marketingCost +
2659
+ categoryCosts.agencyFeesCost +
2660
+ categoryCosts.opexCost +
2661
+ categoryCosts.totalOtherCost);
2662
+ };
2663
+ it('should maintain identity with single order-level COGS entry', () => {
2664
+ const configs = createConfigs({
2665
+ order: {
2666
+ entries: [
2667
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 25, category: 'cogs' }),
2668
+ ],
2669
+ },
2670
+ });
2671
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2672
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2673
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2674
+ expect(sumOfCategories).toBe(inputTotal);
2675
+ expect(sumOfCategories).toBe(25);
2676
+ });
2677
+ it('should maintain identity with single line-item-level entry', () => {
2678
+ const lineItems = [
2679
+ createLineItem({ lineItemId: 'li-1', quantity: 3 }),
2680
+ createLineItem({ lineItemId: 'li-2', quantity: 2 }),
2681
+ ];
2682
+ const context = createContext({ lineItems, totalQuantity: 5 });
2683
+ const configs = createConfigs({
2684
+ order: {
2685
+ entries: [
2686
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 4, category: 'fulfillment' }),
2687
+ ],
2688
+ },
2689
+ });
2690
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2691
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2692
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2693
+ expect(sumOfCategories).toBe(inputTotal);
2694
+ expect(sumOfCategories).toBe(20);
2695
+ });
2696
+ it('should maintain identity with mixed order-level and line-item-level entries across all categories', () => {
2697
+ const lineItems = [
2698
+ createLineItem({ lineItemId: 'li-1', quantity: 2 }),
2699
+ createLineItem({ lineItemId: 'li-2', quantity: 3, refundedQuantity: 1 }),
2700
+ ];
2701
+ const context = createContext({
2702
+ lineItems,
2703
+ totalQuantity: 5,
2704
+ totalWeight: 1000,
2705
+ totalProductCost: 50,
2706
+ totalShipping: 10,
2707
+ totalPrice: 100,
2708
+ grossSalePrice: 120,
2709
+ netSalePrice: 90,
2710
+ hasRefund: true,
2711
+ });
2712
+ const configs = createConfigs({
2713
+ order: {
2714
+ entries: [
2715
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'cogs' }),
2716
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, { flatAmount: 2, category: 'fulfillment' }),
2717
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: 5, category: 'transaction' }),
2718
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, { flatAmount: 1, category: 'marketing' }),
2719
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, { flatAmount: 15, category: 'agency_fees' }),
2720
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, { percentRate: 10, category: 'opex' }),
2721
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, { flatAmount: 3, category: 'other' }),
2722
+ ],
2723
+ },
2724
+ });
2725
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2726
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2727
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2728
+ expect(sumOfCategories).toBe(inputTotal);
2729
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2730
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(10);
2731
+ expect(result.categoryCosts.otherTransactionCost).toBe(5);
2732
+ expect(result.categoryCosts.marketingCost).toBe(5);
2733
+ expect(result.categoryCosts.agencyFeesCost).toBe(15);
2734
+ expect(result.categoryCosts.opexCost).toBe(5);
2735
+ expect(result.categoryCosts.totalOtherCost).toBe(3);
2736
+ expect(sumOfCategories).toBe(53);
2737
+ });
2738
+ it('should maintain identity when entries are filtered out by date range', () => {
2739
+ const configs = createConfigs({
2740
+ order: {
2741
+ entries: [
2742
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2743
+ flatAmount: 10,
2744
+ category: 'cogs',
2745
+ effectiveFrom: '2024-01-01T00:00:00Z',
2746
+ effectiveTo: '2024-12-31T23:59:59Z',
2747
+ }),
2748
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2749
+ flatAmount: 20,
2750
+ category: 'fulfillment',
2751
+ effectiveFrom: '2025-01-01T00:00:00Z',
2752
+ }),
2753
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2754
+ flatAmount: 30,
2755
+ category: 'marketing',
2756
+ effectiveFrom: '2023-01-01T00:00:00Z',
2757
+ effectiveTo: '2023-12-31T23:59:59Z',
2758
+ }),
2759
+ ],
2760
+ },
2761
+ });
2762
+ const context = createContext({ orderCreatedAt: '2024-06-15T10:00:00Z' });
2763
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2764
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2765
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2766
+ expect(sumOfCategories).toBe(inputTotal);
2767
+ expect(sumOfCategories).toBe(10);
2768
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2769
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
2770
+ expect(result.categoryCosts.marketingCost).toBe(0);
2771
+ });
2772
+ it('should maintain identity when entries are filtered out by cost filters', () => {
2773
+ const lineItems = [
2774
+ createLineItem({
2775
+ lineItemId: 'li-1',
2776
+ quantity: 2,
2777
+ filterContext: { tags: ['sale'], productId: '100', variantId: '200' },
2778
+ }),
2779
+ createLineItem({
2780
+ lineItemId: 'li-2',
2781
+ quantity: 3,
2782
+ filterContext: { tags: ['premium'], productId: '101', variantId: '201' },
2783
+ }),
2784
+ ];
2785
+ const context = createContext({
2786
+ lineItems,
2787
+ totalQuantity: 5,
2788
+ orderFilterContext: {
2789
+ tags: ['wholesale'],
2790
+ paymentGateways: ['stripe'],
2791
+ shippingTitles: ['Standard'],
2792
+ },
2793
+ });
2794
+ const configs = createConfigs({
2795
+ order: {
2796
+ entries: [
2797
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2798
+ flatAmount: 10,
2799
+ category: 'cogs',
2800
+ filters: { orderTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['wholesale'] } },
2801
+ }),
2802
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
2803
+ flatAmount: 20,
2804
+ category: 'fulfillment',
2805
+ filters: { orderTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['retail'] } },
2806
+ }),
2807
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
2808
+ flatAmount: 5,
2809
+ category: 'transaction',
2810
+ filters: { productTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['sale'] } },
2811
+ }),
2812
+ ],
2813
+ },
2814
+ });
2815
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2816
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2817
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2818
+ expect(sumOfCategories).toBe(inputTotal);
2819
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2820
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
2821
+ expect(result.categoryCosts.otherTransactionCost).toBe(10);
2822
+ expect(sumOfCategories).toBe(20);
2823
+ });
2824
+ it('should maintain identity with multiple entries in same category', () => {
2825
+ const configs = createConfigs({
2826
+ order: {
2827
+ entries: [
2828
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'e1', flatAmount: 10, category: 'cogs' }),
2829
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'e2', flatAmount: 15, category: 'cogs' }),
2830
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'e3', flatAmount: 5, category: 'cogs' }),
2831
+ ],
2832
+ },
2833
+ });
2834
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2835
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2836
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2837
+ expect(sumOfCategories).toBe(inputTotal);
2838
+ expect(result.categoryCosts.totalOtherCogs).toBe(30);
2839
+ expect(sumOfCategories).toBe(30);
2840
+ });
2841
+ it('should maintain identity with legacy category mappings', () => {
2842
+ const configs = createConfigs({
2843
+ order: {
2844
+ entries: [
2845
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'payment_fees' }),
2846
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 20, category: 'packaging' }),
2847
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 5, category: 'transaction' }),
2848
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 8, category: 'fulfillment' }),
2849
+ ],
2850
+ },
2851
+ });
2852
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2853
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2854
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2855
+ expect(sumOfCategories).toBe(inputTotal);
2856
+ expect(result.categoryCosts.otherTransactionCost).toBe(15);
2857
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(28);
2858
+ expect(sumOfCategories).toBe(43);
2859
+ });
2860
+ it('should maintain identity with empty config', () => {
2861
+ const configs = createConfigs({
2862
+ order: { entries: [] },
2863
+ });
2864
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
2865
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2866
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2867
+ expect(sumOfCategories).toBe(inputTotal);
2868
+ expect(sumOfCategories).toBe(0);
2869
+ });
2870
+ it('should maintain identity with null config', () => {
2871
+ const result = CostCalculatorService.calculateOrderCosts(null, createContext(), 'USD', emptyRatesMap);
2872
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2873
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2874
+ expect(sumOfCategories).toBe(inputTotal);
2875
+ expect(sumOfCategories).toBe(0);
2876
+ });
2877
+ it('should maintain identity with currency conversion', () => {
2878
+ const exchangeRates = {
2879
+ USD: { EUR: 0.85, GBP: 0.73 },
2880
+ EUR: { USD: 1.18, GBP: 0.86 },
2881
+ };
2882
+ const configs = createConfigs({
2883
+ order: {
2884
+ entries: [
2885
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'cogs', currency: 'EUR' }),
2886
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 20, category: 'fulfillment', currency: 'GBP' }),
2887
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 15, category: 'marketing', currency: 'USD' }),
2888
+ ],
2889
+ },
2890
+ });
2891
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', exchangeRates);
2892
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2893
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2894
+ expect(sumOfCategories).toBeCloseTo(inputTotal, 5);
2895
+ expect(result.categoryCosts.totalOtherCogs).toBeCloseTo(10 * 1.18, 5);
2896
+ expect(result.categoryCosts.marketingCost).toBe(15);
2897
+ });
2898
+ it('should maintain identity with percentage-based entries', () => {
2899
+ const context = createContext({
2900
+ totalProductCost: 100,
2901
+ totalShipping: 50,
2902
+ totalPrice: 200,
2903
+ grossSalePrice: 250,
2904
+ netSalePrice: 180,
2905
+ });
2906
+ const configs = createConfigs({
2907
+ order: {
2908
+ entries: [
2909
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, { percentRate: 10, category: 'cogs' }),
2910
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_SHIPPING, { percentRate: 20, category: 'fulfillment' }),
2911
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, { percentRate: 5, category: 'transaction' }),
2912
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, { percentRate: 8, category: 'marketing' }),
2913
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_NET_SALES, { percentRate: 3, category: 'opex' }),
2914
+ ],
2915
+ },
2916
+ });
2917
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2918
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2919
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2920
+ expect(sumOfCategories).toBeCloseTo(inputTotal, 5);
2921
+ expect(result.categoryCosts.totalOtherCogs).toBe(10);
2922
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(10);
2923
+ expect(result.categoryCosts.otherTransactionCost).toBe(10);
2924
+ expect(result.categoryCosts.marketingCost).toBe(20);
2925
+ expect(result.categoryCosts.opexCost).toBe(5.4);
2926
+ expect(sumOfCategories).toBe(55.4);
2927
+ });
2928
+ it('should maintain identity with weight-based entries', () => {
2929
+ const context = createContext({ totalWeight: 1500 });
2930
+ const configs = createConfigs({
2931
+ order: {
2932
+ entries: [
2933
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2934
+ flatAmount: 0.5,
2935
+ weightUnit: WEIGHT_UNIT.KILOGRAMS,
2936
+ category: 'fulfillment',
2937
+ }),
2938
+ ],
2939
+ },
2940
+ });
2941
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2942
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2943
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2944
+ expect(sumOfCategories).toBeCloseTo(inputTotal, 5);
2945
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0.75);
2946
+ });
2947
+ it('should maintain identity with weight-tiered entries', () => {
2948
+ const context = createContext({ totalWeight: 750 });
2949
+ const configs = createConfigs({
2950
+ order: {
2951
+ entries: [
2952
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
2953
+ category: 'fulfillment',
2954
+ weightUnit: WEIGHT_UNIT.GRAMS,
2955
+ weightIntervals: [
2956
+ { weightMin: 0, weightMax: 500, flatAmount: 5 },
2957
+ { weightMin: 501, weightMax: 1000, flatAmount: 8 },
2958
+ { weightMin: 1001, weightMax: null, flatAmount: 12 },
2959
+ ],
2960
+ }),
2961
+ ],
2962
+ },
2963
+ });
2964
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2965
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2966
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2967
+ expect(sumOfCategories).toBeCloseTo(inputTotal, 5);
2968
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(8);
2969
+ });
2970
+ it('should maintain identity with quantity-tiered entries', () => {
2971
+ const context = createContext({
2972
+ lineItems: [
2973
+ createLineItem({ lineItemId: '1', quantity: 5 }),
2974
+ createLineItem({ lineItemId: '2', quantity: 15 }),
2975
+ ],
2976
+ });
2977
+ const configs = createConfigs({
2978
+ order: {
2979
+ entries: [
2980
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
2981
+ category: 'fulfillment',
2982
+ quantityIntervals: [
2983
+ { quantityMin: 1, quantityMax: 10, flatAmount: 10 },
2984
+ { quantityMin: 11, quantityMax: 50, flatAmount: 8 },
2985
+ { quantityMin: 51, quantityMax: null, flatAmount: 5 },
2986
+ ],
2987
+ }),
2988
+ ],
2989
+ },
2990
+ });
2991
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
2992
+ const sumOfCategories = sumCategoryCosts(result.categoryCosts);
2993
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
2994
+ expect(sumOfCategories).toBeCloseTo(inputTotal, 5);
2995
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(18);
2996
+ });
2997
+ });
2998
+ describe('no double-counting between categories', () => {
2999
+ it('should route each entry to exactly one category', () => {
3000
+ const configs = createConfigs({
3001
+ order: {
3002
+ entries: [
3003
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { id: 'e1', flatAmount: 100, category: 'cogs' }),
3004
+ ],
3005
+ },
3006
+ });
3007
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
3008
+ expect(result.categoryCosts.totalOtherCogs).toBe(100);
3009
+ expect(result.categoryCosts.otherFulfillmentCost).toBe(0);
3010
+ expect(result.categoryCosts.otherTransactionCost).toBe(0);
3011
+ expect(result.categoryCosts.marketingCost).toBe(0);
3012
+ expect(result.categoryCosts.agencyFeesCost).toBe(0);
3013
+ expect(result.categoryCosts.opexCost).toBe(0);
3014
+ expect(result.categoryCosts.totalOtherCost).toBe(0);
3015
+ });
3016
+ it('should not double-count when entry matches multiple conditions', () => {
3017
+ const lineItems = [
3018
+ createLineItem({
3019
+ lineItemId: 'li-1',
3020
+ quantity: 2,
3021
+ refundedQuantity: 1,
3022
+ filterContext: { tags: ['sale', 'premium'], productId: '100', variantId: '200', vendor: 'acme' },
3023
+ }),
3024
+ ];
3025
+ const context = createContext({
3026
+ lineItems,
3027
+ totalQuantity: 2,
3028
+ hasRefund: true,
3029
+ orderFilterContext: {
3030
+ tags: ['wholesale', 'vip'],
3031
+ paymentGateways: ['stripe', 'paypal'],
3032
+ shippingTitles: ['Express', 'Standard'],
3033
+ },
3034
+ });
3035
+ const configs = createConfigs({
3036
+ order: {
3037
+ entries: [
3038
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
3039
+ flatAmount: 50,
3040
+ category: 'transaction',
3041
+ filters: {
3042
+ orderTags: { mode: COST_FILTER_MODE.INCLUDE, values: ['wholesale', 'vip'] },
3043
+ },
3044
+ }),
3045
+ ],
3046
+ },
3047
+ });
3048
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
3049
+ expect(result.categoryCosts.otherTransactionCost).toBe(50);
3050
+ expect(result.orderLevelOtherCost + result.lineItemLevelOtherCost).toBe(50);
3051
+ });
3052
+ });
3053
+ describe('handling/gateway/shipping costs are separate from category costs', () => {
3054
+ it('should not include handling costs in category cost sum', () => {
3055
+ const lineItems = [
3056
+ createLineItem({ lineItemId: 'li-1', quantity: 2, variantHandlingFee: 5 }),
3057
+ createLineItem({ lineItemId: 'li-2', quantity: 1, variantHandlingFee: 3 }),
3058
+ ];
3059
+ const configs = createConfigs({
3060
+ globalProductOverride: { globalHandlingFee: 2, currency: 'USD' },
3061
+ order: {
3062
+ entries: [
3063
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'cogs' }),
3064
+ ],
3065
+ },
3066
+ });
3067
+ const context = createContext({ lineItems, totalQuantity: 3 });
3068
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
3069
+ const sumOfCategories = result.categoryCosts.totalOtherCogs +
3070
+ result.categoryCosts.otherFulfillmentCost +
3071
+ result.categoryCosts.otherTransactionCost +
3072
+ result.categoryCosts.marketingCost +
3073
+ result.categoryCosts.agencyFeesCost +
3074
+ result.categoryCosts.opexCost +
3075
+ result.categoryCosts.totalOtherCost;
3076
+ expect(sumOfCategories).toBe(10);
3077
+ expect(result.totalHandlingCost).toBe(17);
3078
+ expect(result.lineItemLevelHandlingCost).toBe(17);
3079
+ });
3080
+ it('should not include gateway costs in category cost sum', () => {
3081
+ const configs = createConfigs({
3082
+ gateway: {
3083
+ entries: [
3084
+ { id: 'g1', name: 'Stripe', gatewayName: 'stripe', flatAmount: 0.30, percentRate: 2.9, currency: 'USD' },
3085
+ ],
3086
+ },
3087
+ order: {
3088
+ entries: [
3089
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'fulfillment' }),
3090
+ ],
3091
+ },
3092
+ });
3093
+ const context = createContext({ totalPrice: 100 });
3094
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', emptyRatesMap);
3095
+ const sumOfCategories = result.categoryCosts.totalOtherCogs +
3096
+ result.categoryCosts.otherFulfillmentCost +
3097
+ result.categoryCosts.otherTransactionCost +
3098
+ result.categoryCosts.marketingCost +
3099
+ result.categoryCosts.agencyFeesCost +
3100
+ result.categoryCosts.opexCost +
3101
+ result.categoryCosts.totalOtherCost;
3102
+ expect(sumOfCategories).toBe(10);
3103
+ expect(result.totalGatewayCost).toBe(3.2);
3104
+ });
3105
+ it('should not include shipping costs in category cost sum', () => {
3106
+ const configs = createConfigs({
3107
+ shipping: {
3108
+ method: SHIPPING_METHOD.FIXED_RATE,
3109
+ fixedRateAmount: 15,
3110
+ currency: 'USD',
3111
+ },
3112
+ order: {
3113
+ entries: [
3114
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, { flatAmount: 10, category: 'marketing' }),
3115
+ ],
3116
+ },
3117
+ });
3118
+ const result = CostCalculatorService.calculateOrderCosts(configs, createContext(), 'USD', emptyRatesMap);
3119
+ const sumOfCategories = result.categoryCosts.totalOtherCogs +
3120
+ result.categoryCosts.otherFulfillmentCost +
3121
+ result.categoryCosts.otherTransactionCost +
3122
+ result.categoryCosts.marketingCost +
3123
+ result.categoryCosts.agencyFeesCost +
3124
+ result.categoryCosts.opexCost +
3125
+ result.categoryCosts.totalOtherCost;
3126
+ expect(sumOfCategories).toBe(10);
3127
+ expect(result.totalShippingCost).toBe(15);
3128
+ });
3129
+ });
3130
+ describe('stress test: large realistic order with all cost types', () => {
3131
+ it('should maintain accounting identity with complex realistic order', () => {
3132
+ const lineItems = [
3133
+ createLineItem({
3134
+ lineItemId: 'li-1',
3135
+ quantity: 5,
3136
+ refundedQuantity: 0,
3137
+ variantHandlingFee: 1.50,
3138
+ filterContext: { tags: ['electronics'], productId: '100', variantId: '200', vendor: 'Samsung' },
3139
+ }),
3140
+ createLineItem({
3141
+ lineItemId: 'li-2',
3142
+ quantity: 3,
3143
+ refundedQuantity: 1,
3144
+ variantHandlingFee: 0.75,
3145
+ filterContext: { tags: ['accessories'], productId: '101', variantId: '201', vendor: 'Generic' },
3146
+ }),
3147
+ createLineItem({
3148
+ lineItemId: 'li-3',
3149
+ quantity: 2,
3150
+ refundedQuantity: 0,
3151
+ variantHandlingFee: 2.00,
3152
+ filterContext: { tags: ['premium', 'electronics'], productId: '102', variantId: '202', vendor: 'Apple' },
3153
+ }),
3154
+ ];
3155
+ const context = createContext({
3156
+ orderCreatedAt: '2024-06-15T10:00:00Z',
3157
+ totalPrice: 850.00,
3158
+ grossSalePrice: 920.00,
3159
+ netSalePrice: 780.00,
3160
+ totalProductCost: 425.00,
3161
+ totalShipping: 25.00,
3162
+ totalWeight: 2500,
3163
+ totalQuantity: 10,
3164
+ hasRefund: true,
3165
+ orderFilterContext: {
3166
+ tags: ['online', 'marketing-campaign'],
3167
+ paymentGateways: ['stripe'],
3168
+ shippingTitles: ['Express Shipping'],
3169
+ shipCountryCode: 'US',
3170
+ },
3171
+ lineItems,
3172
+ });
3173
+ const exchangeRates = {
3174
+ USD: { EUR: 0.85, GBP: 0.73, CAD: 1.35 },
3175
+ EUR: { USD: 1.18 },
3176
+ GBP: { USD: 1.37 },
3177
+ CAD: { USD: 0.74 },
3178
+ };
3179
+ const configs = createConfigs({
3180
+ globalProductOverride: {
3181
+ globalHandlingFee: 0.50,
3182
+ currency: 'USD',
3183
+ },
3184
+ gateway: {
3185
+ entries: [
3186
+ {
3187
+ id: 'stripe',
3188
+ name: 'Stripe',
3189
+ gatewayName: 'stripe',
3190
+ flatAmount: 0.30,
3191
+ percentRate: 2.9,
3192
+ currency: 'USD',
3193
+ },
3194
+ ],
3195
+ },
3196
+ shipping: {
3197
+ method: SHIPPING_METHOD.SHIPPING_PROFILES,
3198
+ profiles: [
3199
+ {
3200
+ id: 'us-flat',
3201
+ name: 'US Flat Rate',
3202
+ countries: ['US'],
3203
+ isDefault: false,
3204
+ rateType: SHIPPING_RATE_TYPE.FLAT,
3205
+ flatAmount: 12.00,
3206
+ currency: 'USD',
3207
+ },
3208
+ {
3209
+ id: 'default',
3210
+ name: 'Default',
3211
+ countries: [],
3212
+ isDefault: true,
3213
+ rateType: SHIPPING_RATE_TYPE.WEIGHT_TIERED,
3214
+ weightUnit: WEIGHT_UNIT.KILOGRAMS,
3215
+ weightTiers: [
3216
+ { weightMin: 0, weightMax: 1, flatAmount: 8 },
3217
+ { weightMin: 1.001, weightMax: 5, flatAmount: 15 },
3218
+ { weightMin: 5.001, weightMax: null, flatAmount: 25 },
3219
+ ],
3220
+ currency: 'USD',
3221
+ },
3222
+ ],
3223
+ },
3224
+ order: {
3225
+ entries: [
3226
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
3227
+ id: 'packaging-base',
3228
+ flatAmount: 2.00,
3229
+ category: 'cogs',
3230
+ }),
3231
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_COGS, {
3232
+ id: 'inventory-shrinkage',
3233
+ percentRate: 2,
3234
+ category: 'cogs',
3235
+ }),
3236
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_LINE_ITEM, {
3237
+ id: 'pick-pack',
3238
+ flatAmount: 0.75,
3239
+ category: 'fulfillment',
3240
+ }),
3241
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_ORDER_WEIGHT, {
3242
+ id: 'packing-materials',
3243
+ flatAmount: 0.002,
3244
+ category: 'fulfillment',
3245
+ }),
3246
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_TOTAL_SALES, {
3247
+ id: 'platform-fee',
3248
+ percentRate: 1.5,
3249
+ category: 'transaction',
3250
+ }),
3251
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_GROSS_SALES, {
3252
+ id: 'affiliate-commission',
3253
+ percentRate: 5,
3254
+ category: 'marketing',
3255
+ }),
3256
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_ORDER, {
3257
+ id: 'agency-retainer-allocation',
3258
+ flatAmount: 8.00,
3259
+ currency: 'EUR',
3260
+ category: 'agency_fees',
3261
+ }),
3262
+ createEntry(ORDER_COST_ENTRY_TYPE.PCT_NET_SALES, {
3263
+ id: 'overhead-allocation',
3264
+ percentRate: 3,
3265
+ category: 'opex',
3266
+ }),
3267
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_ORDER, {
3268
+ id: 'refund-processing',
3269
+ flatAmount: 5.00,
3270
+ category: 'opex',
3271
+ }),
3272
+ createEntry(ORDER_COST_ENTRY_TYPE.PER_REFUND_LINE_ITEM, {
3273
+ id: 'restocking-fee',
3274
+ flatAmount: 3.00,
3275
+ category: 'other',
3276
+ }),
3277
+ createEntry(ORDER_COST_ENTRY_TYPE.BY_LINE_ITEM_QUANTITY, {
3278
+ id: 'misc-per-unit',
3279
+ quantityIntervals: [
3280
+ { quantityMin: 1, quantityMax: 5, flatAmount: 2 },
3281
+ { quantityMin: 6, quantityMax: 20, flatAmount: 1.5 },
3282
+ { quantityMin: 21, quantityMax: null, flatAmount: 1 },
3283
+ ],
3284
+ category: 'other',
3285
+ }),
3286
+ ],
3287
+ },
3288
+ });
3289
+ const result = CostCalculatorService.calculateOrderCosts(configs, context, 'USD', exchangeRates);
3290
+ const sumOfCategories = result.categoryCosts.totalOtherCogs +
3291
+ result.categoryCosts.otherFulfillmentCost +
3292
+ result.categoryCosts.otherTransactionCost +
3293
+ result.categoryCosts.marketingCost +
3294
+ result.categoryCosts.agencyFeesCost +
3295
+ result.categoryCosts.opexCost +
3296
+ result.categoryCosts.totalOtherCost;
3297
+ const inputTotal = result.orderLevelOtherCost + result.lineItemLevelOtherCost;
3298
+ expect(sumOfCategories).toBeCloseTo(inputTotal, 5);
3299
+ expect(Number.isFinite(sumOfCategories)).toBe(true);
3300
+ expect(sumOfCategories).toBeGreaterThanOrEqual(0);
3301
+ expect(Number.isFinite(result.totalHandlingCost)).toBe(true);
3302
+ expect(Number.isFinite(result.totalGatewayCost)).toBe(true);
3303
+ expect(Number.isFinite(result.totalShippingCost)).toBe(true);
3304
+ expect(result.categoryCosts.totalOtherCogs).toBeGreaterThan(0);
3305
+ expect(result.categoryCosts.otherFulfillmentCost).toBeGreaterThan(0);
3306
+ expect(result.categoryCosts.otherTransactionCost).toBeGreaterThan(0);
3307
+ expect(result.categoryCosts.marketingCost).toBeGreaterThan(0);
3308
+ expect(result.categoryCosts.agencyFeesCost).toBeGreaterThan(0);
3309
+ expect(result.categoryCosts.opexCost).toBeGreaterThan(0);
3310
+ expect(result.categoryCosts.totalOtherCost).toBeGreaterThan(0);
3311
+ expect(result.totalHandlingCost).toBeGreaterThan(0);
3312
+ expect(result.totalGatewayCost).toBeGreaterThan(0);
3313
+ expect(result.totalShippingCost).toBeGreaterThan(0);
3314
+ });
3315
+ });
3316
+ });
3317
+ });
3318
+ //# sourceMappingURL=cost-calculator-service.spec.js.map