@powerhousedao/service-offering 1.0.0-dev.4 → 1.0.0-dev.6

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 (235) hide show
  1. package/dist/document-models/facet/v1/actions.d.ts +3 -1
  2. package/dist/document-models/facet/v1/actions.d.ts.map +1 -1
  3. package/dist/document-models/facet/v1/gen/controller.d.ts +4 -0
  4. package/dist/document-models/facet/v1/gen/controller.d.ts.map +1 -0
  5. package/dist/document-models/facet/v1/gen/controller.js +3 -0
  6. package/dist/document-models/facet/v1/gen/document-model.d.ts.map +1 -1
  7. package/dist/document-models/facet/v1/gen/document-model.js +31 -7
  8. package/dist/document-models/facet/v1/gen/document-schema.d.ts +6 -6
  9. package/dist/document-models/facet/v1/gen/index.d.ts +1 -0
  10. package/dist/document-models/facet/v1/gen/index.d.ts.map +1 -1
  11. package/dist/document-models/facet/v1/gen/index.js +1 -0
  12. package/dist/document-models/facet/v1/gen/option-management/error.d.ts +27 -1
  13. package/dist/document-models/facet/v1/gen/option-management/error.d.ts.map +1 -1
  14. package/dist/document-models/facet/v1/gen/option-management/error.js +23 -1
  15. package/dist/document-models/facet/v1/gen/ph-factories.js +2 -2
  16. package/dist/document-models/facet/v1/gen/schema/types.d.ts +2 -2
  17. package/dist/document-models/facet/v1/gen/schema/types.d.ts.map +1 -1
  18. package/dist/document-models/facet/v1/gen/schema/zod.js +2 -2
  19. package/dist/document-models/facet/v1/gen/utils.js +2 -2
  20. package/dist/document-models/facet/v1/module.d.ts +1 -1
  21. package/dist/document-models/facet/v1/module.d.ts.map +1 -1
  22. package/dist/document-models/facet/v1/module.js +4 -1
  23. package/dist/document-models/resource-instance/v1/actions.d.ts +4 -1
  24. package/dist/document-models/resource-instance/v1/actions.d.ts.map +1 -1
  25. package/dist/document-models/resource-instance/v1/gen/configuration-management/error.d.ts +34 -1
  26. package/dist/document-models/resource-instance/v1/gen/configuration-management/error.d.ts.map +1 -1
  27. package/dist/document-models/resource-instance/v1/gen/configuration-management/error.js +32 -1
  28. package/dist/document-models/resource-instance/v1/gen/controller.d.ts +4 -0
  29. package/dist/document-models/resource-instance/v1/gen/controller.d.ts.map +1 -0
  30. package/dist/document-models/resource-instance/v1/gen/controller.js +3 -0
  31. package/dist/document-models/resource-instance/v1/gen/document-model.d.ts.map +1 -1
  32. package/dist/document-models/resource-instance/v1/gen/document-model.js +222 -70
  33. package/dist/document-models/resource-instance/v1/gen/document-schema.d.ts +0 -6
  34. package/dist/document-models/resource-instance/v1/gen/document-schema.d.ts.map +1 -1
  35. package/dist/document-models/resource-instance/v1/gen/index.d.ts +1 -0
  36. package/dist/document-models/resource-instance/v1/gen/index.d.ts.map +1 -1
  37. package/dist/document-models/resource-instance/v1/gen/index.js +1 -0
  38. package/dist/document-models/resource-instance/v1/gen/instance-management/actions.d.ts +6 -2
  39. package/dist/document-models/resource-instance/v1/gen/instance-management/actions.d.ts.map +1 -1
  40. package/dist/document-models/resource-instance/v1/gen/instance-management/creators.d.ts +3 -2
  41. package/dist/document-models/resource-instance/v1/gen/instance-management/creators.d.ts.map +1 -1
  42. package/dist/document-models/resource-instance/v1/gen/instance-management/creators.js +2 -1
  43. package/dist/document-models/resource-instance/v1/gen/instance-management/error.d.ts +98 -1
  44. package/dist/document-models/resource-instance/v1/gen/instance-management/error.d.ts.map +1 -1
  45. package/dist/document-models/resource-instance/v1/gen/instance-management/error.js +112 -1
  46. package/dist/document-models/resource-instance/v1/gen/instance-management/operations.d.ts +2 -1
  47. package/dist/document-models/resource-instance/v1/gen/instance-management/operations.d.ts.map +1 -1
  48. package/dist/document-models/resource-instance/v1/gen/ph-factories.d.ts.map +1 -1
  49. package/dist/document-models/resource-instance/v1/gen/ph-factories.js +0 -2
  50. package/dist/document-models/resource-instance/v1/gen/reducer.d.ts.map +1 -1
  51. package/dist/document-models/resource-instance/v1/gen/reducer.js +6 -1
  52. package/dist/document-models/resource-instance/v1/gen/schema/types.d.ts +3 -3
  53. package/dist/document-models/resource-instance/v1/gen/schema/types.d.ts.map +1 -1
  54. package/dist/document-models/resource-instance/v1/gen/schema/zod.d.ts +2 -1
  55. package/dist/document-models/resource-instance/v1/gen/schema/zod.d.ts.map +1 -1
  56. package/dist/document-models/resource-instance/v1/gen/schema/zod.js +5 -3
  57. package/dist/document-models/resource-instance/v1/gen/utils.d.ts.map +1 -1
  58. package/dist/document-models/resource-instance/v1/gen/utils.js +0 -2
  59. package/dist/document-models/resource-instance/v1/module.d.ts +1 -1
  60. package/dist/document-models/resource-instance/v1/module.d.ts.map +1 -1
  61. package/dist/document-models/resource-instance/v1/module.js +4 -1
  62. package/dist/document-models/resource-instance/v1/src/reducers/configuration-management.d.ts.map +1 -1
  63. package/dist/document-models/resource-instance/v1/src/reducers/configuration-management.js +57 -53
  64. package/dist/document-models/resource-instance/v1/src/reducers/instance-management.d.ts.map +1 -1
  65. package/dist/document-models/resource-instance/v1/src/reducers/instance-management.js +57 -21
  66. package/dist/document-models/resource-instance/v1/tests/instance-management.test.js +11 -1
  67. package/dist/document-models/resource-template/v1/actions.d.ts +3 -1
  68. package/dist/document-models/resource-template/v1/actions.d.ts.map +1 -1
  69. package/dist/document-models/resource-template/v1/gen/audience-management/error.d.ts +20 -1
  70. package/dist/document-models/resource-template/v1/gen/audience-management/error.d.ts.map +1 -1
  71. package/dist/document-models/resource-template/v1/gen/audience-management/error.js +16 -1
  72. package/dist/document-models/resource-template/v1/gen/controller.d.ts +4 -0
  73. package/dist/document-models/resource-template/v1/gen/controller.d.ts.map +1 -0
  74. package/dist/document-models/resource-template/v1/gen/controller.js +3 -0
  75. package/dist/document-models/resource-template/v1/gen/document-model.d.ts.map +1 -1
  76. package/dist/document-models/resource-template/v1/gen/document-model.js +207 -89
  77. package/dist/document-models/resource-template/v1/gen/document-schema.d.ts +12 -12
  78. package/dist/document-models/resource-template/v1/gen/facet-targeting/error.d.ts +27 -1
  79. package/dist/document-models/resource-template/v1/gen/facet-targeting/error.d.ts.map +1 -1
  80. package/dist/document-models/resource-template/v1/gen/facet-targeting/error.js +23 -1
  81. package/dist/document-models/resource-template/v1/gen/index.d.ts +1 -0
  82. package/dist/document-models/resource-template/v1/gen/index.d.ts.map +1 -1
  83. package/dist/document-models/resource-template/v1/gen/index.js +1 -0
  84. package/dist/document-models/resource-template/v1/gen/option-group-management/error.d.ts +27 -1
  85. package/dist/document-models/resource-template/v1/gen/option-group-management/error.d.ts.map +1 -1
  86. package/dist/document-models/resource-template/v1/gen/option-group-management/error.js +23 -1
  87. package/dist/document-models/resource-template/v1/gen/ph-factories.js +3 -3
  88. package/dist/document-models/resource-template/v1/gen/schema/types.d.ts +4 -4
  89. package/dist/document-models/resource-template/v1/gen/schema/types.d.ts.map +1 -1
  90. package/dist/document-models/resource-template/v1/gen/schema/zod.js +4 -4
  91. package/dist/document-models/resource-template/v1/gen/service-management/error.d.ts +51 -1
  92. package/dist/document-models/resource-template/v1/gen/service-management/error.d.ts.map +1 -1
  93. package/dist/document-models/resource-template/v1/gen/service-management/error.js +49 -1
  94. package/dist/document-models/resource-template/v1/gen/utils.js +3 -3
  95. package/dist/document-models/resource-template/v1/module.d.ts +1 -1
  96. package/dist/document-models/resource-template/v1/module.d.ts.map +1 -1
  97. package/dist/document-models/resource-template/v1/module.js +4 -1
  98. package/dist/document-models/resource-template/v1/src/reducers/option-group-management.d.ts.map +1 -1
  99. package/dist/document-models/resource-template/v1/src/reducers/option-group-management.js +2 -21
  100. package/dist/document-models/service-offering/v1/actions.d.ts +3 -1
  101. package/dist/document-models/service-offering/v1/actions.d.ts.map +1 -1
  102. package/dist/document-models/service-offering/v1/gen/controller.d.ts +4 -0
  103. package/dist/document-models/service-offering/v1/gen/controller.d.ts.map +1 -0
  104. package/dist/document-models/service-offering/v1/gen/controller.js +3 -0
  105. package/dist/document-models/service-offering/v1/gen/document-model.d.ts.map +1 -1
  106. package/dist/document-models/service-offering/v1/gen/document-model.js +421 -199
  107. package/dist/document-models/service-offering/v1/gen/document-schema.d.ts +9 -9
  108. package/dist/document-models/service-offering/v1/gen/index.d.ts +1 -0
  109. package/dist/document-models/service-offering/v1/gen/index.d.ts.map +1 -1
  110. package/dist/document-models/service-offering/v1/gen/index.js +1 -0
  111. package/dist/document-models/service-offering/v1/gen/offering/error.d.ts +41 -1
  112. package/dist/document-models/service-offering/v1/gen/offering/error.d.ts.map +1 -1
  113. package/dist/document-models/service-offering/v1/gen/offering/error.js +37 -1
  114. package/dist/document-models/service-offering/v1/gen/option-groups/error.d.ts +55 -1
  115. package/dist/document-models/service-offering/v1/gen/option-groups/error.d.ts.map +1 -1
  116. package/dist/document-models/service-offering/v1/gen/option-groups/error.js +53 -1
  117. package/dist/document-models/service-offering/v1/gen/ph-factories.js +3 -3
  118. package/dist/document-models/service-offering/v1/gen/schema/types.d.ts +134 -61
  119. package/dist/document-models/service-offering/v1/gen/schema/types.d.ts.map +1 -1
  120. package/dist/document-models/service-offering/v1/gen/schema/zod.d.ts +35 -10
  121. package/dist/document-models/service-offering/v1/gen/schema/zod.d.ts.map +1 -1
  122. package/dist/document-models/service-offering/v1/gen/schema/zod.js +182 -64
  123. package/dist/document-models/service-offering/v1/gen/services/error.d.ts +20 -1
  124. package/dist/document-models/service-offering/v1/gen/services/error.d.ts.map +1 -1
  125. package/dist/document-models/service-offering/v1/gen/services/error.js +16 -1
  126. package/dist/document-models/service-offering/v1/gen/tiers/error.d.ts +100 -1
  127. package/dist/document-models/service-offering/v1/gen/tiers/error.d.ts.map +1 -1
  128. package/dist/document-models/service-offering/v1/gen/tiers/error.js +106 -1
  129. package/dist/document-models/service-offering/v1/gen/utils.js +4 -4
  130. package/dist/document-models/service-offering/v1/module.d.ts +1 -1
  131. package/dist/document-models/service-offering/v1/module.d.ts.map +1 -1
  132. package/dist/document-models/service-offering/v1/module.js +4 -1
  133. package/dist/document-models/service-offering/v1/src/reducers/offering.d.ts.map +1 -1
  134. package/dist/document-models/service-offering/v1/src/reducers/offering.js +20 -12
  135. package/dist/document-models/service-offering/v1/src/reducers/option-groups.d.ts.map +1 -1
  136. package/dist/document-models/service-offering/v1/src/reducers/option-groups.js +157 -39
  137. package/dist/document-models/service-offering/v1/src/reducers/services.d.ts.map +1 -1
  138. package/dist/document-models/service-offering/v1/src/reducers/services.js +17 -14
  139. package/dist/document-models/service-offering/v1/src/reducers/tiers.d.ts.map +1 -1
  140. package/dist/document-models/service-offering/v1/src/reducers/tiers.js +111 -78
  141. package/dist/document-models/service-offering/v1/src/utils.d.ts +60 -1
  142. package/dist/document-models/service-offering/v1/src/utils.d.ts.map +1 -1
  143. package/dist/document-models/service-offering/v1/src/utils.js +173 -1
  144. package/dist/document-models/service-offering/v1/tests/option-groups.test.js +1 -1
  145. package/dist/document-models/service-offering/v1/utils.d.ts +3 -0
  146. package/dist/document-models/service-offering/v1/utils.d.ts.map +1 -1
  147. package/dist/document-models/subscription-instance/v1/actions.d.ts +3 -1
  148. package/dist/document-models/subscription-instance/v1/actions.d.ts.map +1 -1
  149. package/dist/document-models/subscription-instance/v1/gen/controller.d.ts +4 -0
  150. package/dist/document-models/subscription-instance/v1/gen/controller.d.ts.map +1 -0
  151. package/dist/document-models/subscription-instance/v1/gen/controller.js +3 -0
  152. package/dist/document-models/subscription-instance/v1/gen/document-model.d.ts.map +1 -1
  153. package/dist/document-models/subscription-instance/v1/gen/document-model.js +488 -246
  154. package/dist/document-models/subscription-instance/v1/gen/document-schema.d.ts +3 -3
  155. package/dist/document-models/subscription-instance/v1/gen/index.d.ts +1 -0
  156. package/dist/document-models/subscription-instance/v1/gen/index.d.ts.map +1 -1
  157. package/dist/document-models/subscription-instance/v1/gen/index.js +1 -0
  158. package/dist/document-models/subscription-instance/v1/gen/metrics/error.d.ts +73 -1
  159. package/dist/document-models/subscription-instance/v1/gen/metrics/error.d.ts.map +1 -1
  160. package/dist/document-models/subscription-instance/v1/gen/metrics/error.js +86 -1
  161. package/dist/document-models/subscription-instance/v1/gen/ph-factories.js +1 -1
  162. package/dist/document-models/subscription-instance/v1/gen/schema/types.d.ts +199 -82
  163. package/dist/document-models/subscription-instance/v1/gen/schema/types.d.ts.map +1 -1
  164. package/dist/document-models/subscription-instance/v1/gen/schema/zod.d.ts +22 -12
  165. package/dist/document-models/subscription-instance/v1/gen/schema/zod.d.ts.map +1 -1
  166. package/dist/document-models/subscription-instance/v1/gen/schema/zod.js +230 -84
  167. package/dist/document-models/subscription-instance/v1/gen/service/error.d.ts +62 -1
  168. package/dist/document-models/subscription-instance/v1/gen/service/error.d.ts.map +1 -1
  169. package/dist/document-models/subscription-instance/v1/gen/service/error.js +60 -1
  170. package/dist/document-models/subscription-instance/v1/gen/service-group/error.d.ts +39 -1
  171. package/dist/document-models/subscription-instance/v1/gen/service-group/error.d.ts.map +1 -1
  172. package/dist/document-models/subscription-instance/v1/gen/service-group/error.js +39 -1
  173. package/dist/document-models/subscription-instance/v1/gen/subscription/error.d.ts +55 -1
  174. package/dist/document-models/subscription-instance/v1/gen/subscription/error.d.ts.map +1 -1
  175. package/dist/document-models/subscription-instance/v1/gen/subscription/error.js +51 -1
  176. package/dist/document-models/subscription-instance/v1/gen/utils.js +2 -2
  177. package/dist/document-models/subscription-instance/v1/module.d.ts +1 -1
  178. package/dist/document-models/subscription-instance/v1/module.d.ts.map +1 -1
  179. package/dist/document-models/subscription-instance/v1/module.js +4 -1
  180. package/dist/document-models/subscription-instance/v1/src/reducers/customer.d.ts.map +1 -1
  181. package/dist/document-models/subscription-instance/v1/src/reducers/customer.js +1 -0
  182. package/dist/document-models/subscription-instance/v1/src/reducers/metrics.d.ts.map +1 -1
  183. package/dist/document-models/subscription-instance/v1/src/reducers/metrics.js +70 -45
  184. package/dist/document-models/subscription-instance/v1/src/reducers/service-group.d.ts.map +1 -1
  185. package/dist/document-models/subscription-instance/v1/src/reducers/service-group.js +108 -30
  186. package/dist/document-models/subscription-instance/v1/src/reducers/service.d.ts.map +1 -1
  187. package/dist/document-models/subscription-instance/v1/src/reducers/service.js +108 -39
  188. package/dist/document-models/subscription-instance/v1/src/reducers/subscription.d.ts.map +1 -1
  189. package/dist/document-models/subscription-instance/v1/src/reducers/subscription.js +193 -35
  190. package/dist/editors/resource-instance-editor/editor.d.ts.map +1 -1
  191. package/dist/editors/resource-instance-editor/editor.js +13 -3
  192. package/dist/editors/service-offering-editor/components/ResourceTemplateSelector.d.ts.map +1 -1
  193. package/dist/editors/service-offering-editor/components/ResourceTemplateSelector.js +4 -2
  194. package/dist/editors/service-offering-editor/components/ServiceCatalog.d.ts.map +1 -1
  195. package/dist/editors/service-offering-editor/components/ServiceCatalog.js +189 -32
  196. package/dist/editors/service-offering-editor/components/TheMatrix.d.ts +1 -1
  197. package/dist/editors/service-offering-editor/components/TheMatrix.d.ts.map +1 -1
  198. package/dist/editors/service-offering-editor/components/TheMatrix.js +295 -140
  199. package/dist/editors/service-offering-editor/components/TierDefinition.d.ts.map +1 -1
  200. package/dist/editors/service-offering-editor/components/TierDefinition.js +2 -0
  201. package/dist/editors/service-offering-editor/components/TierPricingOptionsPanel.js +3 -3
  202. package/dist/editors/service-offering-editor/components/pricing-utils.d.ts.map +1 -1
  203. package/dist/editors/service-offering-editor/components/pricing-utils.js +26 -7
  204. package/dist/editors/subscription-instance-editor/components/BillingPanel.d.ts.map +1 -1
  205. package/dist/editors/subscription-instance-editor/components/BillingPanel.js +4 -4
  206. package/dist/editors/subscription-instance-editor/components/CustomerInfo.d.ts.map +1 -1
  207. package/dist/editors/subscription-instance-editor/components/CustomerInfo.js +3 -2
  208. package/dist/editors/subscription-instance-editor/components/ImportServiceConfigButton.d.ts +3 -0
  209. package/dist/editors/subscription-instance-editor/components/ImportServiceConfigButton.d.ts.map +1 -1
  210. package/dist/editors/subscription-instance-editor/components/ImportServiceConfigButton.js +12 -0
  211. package/dist/editors/subscription-instance-editor/components/MetricActions.d.ts.map +1 -1
  212. package/dist/editors/subscription-instance-editor/components/MetricActions.js +4 -2
  213. package/dist/editors/subscription-instance-editor/components/MockDataButton.d.ts.map +1 -1
  214. package/dist/editors/subscription-instance-editor/components/MockDataButton.js +214 -2
  215. package/dist/editors/subscription-instance-editor/components/OperatorNotes.js +1 -1
  216. package/dist/editors/subscription-instance-editor/components/ServicesPanel.d.ts.map +1 -1
  217. package/dist/editors/subscription-instance-editor/components/ServicesPanel.js +9 -20
  218. package/dist/editors/subscription-instance-editor/components/SubscriptionActions.d.ts.map +1 -1
  219. package/dist/editors/subscription-instance-editor/components/SubscriptionActions.js +8 -9
  220. package/dist/editors/subscription-instance-editor/components/SubscriptionHeader.d.ts.map +1 -1
  221. package/dist/editors/subscription-instance-editor/components/SubscriptionHeader.js +1 -1
  222. package/dist/editors/subscription-instance-editor/components/billing-utils.d.ts +14 -6
  223. package/dist/editors/subscription-instance-editor/components/billing-utils.d.ts.map +1 -1
  224. package/dist/editors/subscription-instance-editor/components/billing-utils.js +19 -23
  225. package/dist/editors/subscription-instance-editor/components/mapOfferingToSubscription.d.ts +16 -2
  226. package/dist/editors/subscription-instance-editor/components/mapOfferingToSubscription.d.ts.map +1 -1
  227. package/dist/editors/subscription-instance-editor/components/mapOfferingToSubscription.js +155 -6
  228. package/dist/powerhouse.manifest.json +29 -3
  229. package/dist/style.css +14 -0
  230. package/dist/subgraphs/resources-services/resolvers.d.ts +1 -1
  231. package/dist/subgraphs/resources-services/resolvers.d.ts.map +1 -1
  232. package/dist/subgraphs/resources-services/resolvers.js +273 -158
  233. package/dist/subgraphs/resources-services/schema.d.ts.map +1 -1
  234. package/dist/subgraphs/resources-services/schema.js +107 -41
  235. package/package.json +22 -18
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useMemo, useEffect, useRef, useCallback } from "react";
3
3
  import { generateId } from "document-model/core";
4
4
  import { usePHToast, } from "@powerhousedao/reactor-browser";
5
+ import {} from "@powerhousedao/service-offering/document-models/service-offering";
5
6
  import { BILLING_CYCLE_SHORT_LABELS, BILLING_CYCLE_LABELS, BILLING_CYCLE_MONTHS, RECURRING_BILLING_CYCLES, formatPrice, detectMajorityCycle, } from "./pricing-utils.js";
6
7
  import { addServiceLevel, updateServiceLevel, addUsageLimit, updateUsageLimit, removeUsageLimit, addService, updateService, } from "../../../document-models/service-offering/v1/gen/creators.js";
8
+ import { getUserSelectionPriceBreakdown, } from "../../../document-models/service-offering/v1/index.js";
7
9
  import { InfoIcon } from "./InfoIcon.js";
8
10
  import { ConfirmDialog } from "./ConfirmDialog.js";
9
11
  const SERVICE_LEVELS = [
@@ -237,44 +239,16 @@ export function TheMatrix({ document, dispatch }) {
237
239
  const addonGroups = useMemo(() => {
238
240
  return optionGroups.filter((g) => g.isAddOn);
239
241
  }, [optionGroups]);
242
+ // Precompute price breakdowns for all tiers using the centralized utility
240
243
  const tierBreakdowns = useMemo(() => {
241
- const months = BILLING_CYCLE_MONTHS[activeBillingCycle];
242
- return tiers.map((tier) => {
243
- const regularGroups = optionGroups.filter((g) => g.costType !== "SETUP" && !g.isAddOn);
244
- let monthlyBase = 0;
245
- const ogBreakdowns = [];
246
- for (const group of regularGroups) {
247
- const tp = group.tierDependentPricing?.find((p) => p.tierId === tier.id);
248
- const amount = tp?.amount ?? group.standalonePricing?.amount ?? 0;
249
- monthlyBase += amount;
250
- ogBreakdowns.push({
251
- optionGroupId: group.id,
252
- cycleAmount: amount * months,
253
- });
254
- }
255
- let addOnTotal = 0;
256
- const addOnBreakdowns = [];
257
- for (const group of optionGroups.filter((g) => g.isAddOn)) {
258
- if (!enabledOptionalGroups.has(group.id))
259
- continue;
260
- const tp = group.tierDependentPricing?.find((p) => p.tierId === tier.id);
261
- const amount = tp?.amount ?? group.standalonePricing?.amount ?? 0;
262
- addOnTotal += amount * months;
263
- addOnBreakdowns.push({
264
- optionGroupId: group.id,
265
- cycleAmount: amount * months,
266
- });
267
- }
268
- const tierCycleTotal = monthlyBase * months;
269
- return {
270
- tierMonthlyBase: monthlyBase,
271
- tierCycleTotal,
272
- tierCurrency: tier.pricing?.currency || "USD",
273
- addOnBreakdowns,
274
- optionGroupBreakdowns: ogBreakdowns,
275
- totals: { grandRecurringTotal: tierCycleTotal + addOnTotal },
276
- };
277
- });
244
+ const addonIds = [...enabledOptionalGroups];
245
+ return tiers.map((tier) => getUserSelectionPriceBreakdown(state, {
246
+ tierId: tier.id,
247
+ billingCycle: activeBillingCycle,
248
+ optionGroupIds: addonIds,
249
+ groupBillingCycleOverrides: groupBillingCycles,
250
+ addonBillingCycleOverrides: addonBillingCycles,
251
+ }));
278
252
  }, [
279
253
  tiers,
280
254
  optionGroups,
@@ -292,10 +266,12 @@ export function TheMatrix({ document, dispatch }) {
292
266
  const getServiceLevelForTier = (serviceId, tier) => {
293
267
  return tier.serviceLevels.find((sl) => sl.serviceId === serviceId);
294
268
  };
295
- const getUniqueMetricsForService = (_serviceId) => {
269
+ const getUniqueMetricsForService = (serviceId) => {
296
270
  const metricsSet = new Set();
297
271
  tiers.forEach((tier) => {
298
- tier.usageLimits.forEach((ul) => metricsSet.add(ul.name));
272
+ tier.usageLimits
273
+ .filter((ul) => ul.serviceId === serviceId)
274
+ .forEach((ul) => metricsSet.add(ul.metric));
299
275
  });
300
276
  return Array.from(metricsSet);
301
277
  };
@@ -309,8 +285,8 @@ export function TheMatrix({ document, dispatch }) {
309
285
  return !isIncludedAnywhere;
310
286
  });
311
287
  }, [services, tiers]);
312
- const getUsageLimitForMetric = (_serviceId, metric, tier) => {
313
- return tier.usageLimits.find((ul) => ul.name === metric);
288
+ const getUsageLimitForMetric = (serviceId, metric, tier) => {
289
+ return tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === metric);
314
290
  };
315
291
  // Derive tier display pricing from precomputed breakdown
316
292
  const getTierDisplayPrice = (tierIdx) => {
@@ -334,11 +310,11 @@ export function TheMatrix({ document, dispatch }) {
334
310
  discountLabel: savingsPercent > 0 ? `SAVE ${savingsPercent}%` : "",
335
311
  };
336
312
  };
337
- const handleSetServiceLevel = (serviceId, tierId, level, existingLevelId, _optionGroupId) => {
313
+ const handleSetServiceLevel = (serviceId, tierId, level, existingLevelId, optionGroupId) => {
338
314
  if (existingLevelId) {
339
315
  dispatch(updateServiceLevel({
340
316
  tierId,
341
- id: existingLevelId,
317
+ serviceLevelId: existingLevelId,
342
318
  level,
343
319
  lastModified: new Date().toISOString(),
344
320
  }));
@@ -346,9 +322,10 @@ export function TheMatrix({ document, dispatch }) {
346
322
  else {
347
323
  dispatch(addServiceLevel({
348
324
  tierId,
349
- id: generateId(),
325
+ serviceLevelId: generateId(),
350
326
  serviceId,
351
327
  level,
328
+ optionGroupId,
352
329
  lastModified: new Date().toISOString(),
353
330
  }));
354
331
  }
@@ -381,12 +358,16 @@ export function TheMatrix({ document, dispatch }) {
381
358
  : undefined,
382
359
  lastModified: now,
383
360
  }));
361
+ // Create ServiceLevelBindings for each selected tier
384
362
  newServiceSelectedTiers.forEach((tierId) => {
385
363
  dispatch(addServiceLevel({
386
364
  tierId,
387
- id: generateId(),
365
+ serviceLevelId: generateId(),
388
366
  serviceId: newServiceId,
389
367
  level: "INCLUDED",
368
+ optionGroupId: addServiceModal.groupId !== UNGROUPED_ID
369
+ ? addServiceModal.groupId
370
+ : undefined,
390
371
  lastModified: now,
391
372
  }));
392
373
  });
@@ -431,20 +412,23 @@ export function TheMatrix({ document, dispatch }) {
431
412
  const existingLevel = tier.serviceLevels.find((sl) => sl.serviceId === editServiceModal.id);
432
413
  const shouldBeIncluded = editServiceSelectedTiers.has(tier.id);
433
414
  if (shouldBeIncluded && !existingLevel) {
415
+ // Add to tier
434
416
  dispatch(addServiceLevel({
435
417
  tierId: tier.id,
436
- id: generateId(),
418
+ serviceLevelId: generateId(),
437
419
  serviceId: editServiceModal.id,
438
420
  level: "INCLUDED",
421
+ optionGroupId: editServiceModal.optionGroupId || undefined,
439
422
  lastModified: now,
440
423
  }));
441
424
  }
442
425
  else if (shouldBeIncluded &&
443
426
  existingLevel &&
444
427
  existingLevel.level !== "INCLUDED") {
428
+ // Update to included
445
429
  dispatch(updateServiceLevel({
446
430
  tierId: tier.id,
447
- id: existingLevel.id,
431
+ serviceLevelId: existingLevel.id,
448
432
  level: "INCLUDED",
449
433
  lastModified: now,
450
434
  }));
@@ -452,9 +436,10 @@ export function TheMatrix({ document, dispatch }) {
452
436
  else if (!shouldBeIncluded &&
453
437
  existingLevel &&
454
438
  existingLevel.level === "INCLUDED") {
439
+ // Remove from tier (set to NOT_INCLUDED)
455
440
  dispatch(updateServiceLevel({
456
441
  tierId: tier.id,
457
- id: existingLevel.id,
442
+ serviceLevelId: existingLevel.id,
458
443
  level: "NOT_INCLUDED",
459
444
  lastModified: now,
460
445
  }));
@@ -494,23 +479,33 @@ export function TheMatrix({ document, dispatch }) {
494
479
  const service = services.find((s) => s.id === serviceId);
495
480
  setMetricResetCycle(service?.isSetupFormation ? "NONE" : "MONTHLY");
496
481
  };
497
- const handleEditMetric = (_serviceId, metric) => {
498
- setMetricModal({ serviceId: _serviceId, metric });
482
+ const handleEditMetric = (serviceId, metric) => {
483
+ setMetricModal({ serviceId, metric });
499
484
  setMetricName(metric);
485
+ // Initialize limits with existing values and track which tiers have this metric
500
486
  const existingLimits = {};
501
487
  const existingPaidLimits = {};
502
488
  const existingOveragePrices = {};
503
489
  const enabledTiers = new Set();
504
490
  let existingUnitName = "";
491
+ let existingResetCycle = "MONTHLY";
505
492
  tiers.forEach((tier) => {
506
- const usageLimit = tier.usageLimits.find((ul) => ul.name === metric);
507
- existingLimits[tier.id] = usageLimit?.limit?.toString() || "";
508
- existingPaidLimits[tier.id] = "";
509
- existingOveragePrices[tier.id] = "";
493
+ const usageLimit = tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === metric);
494
+ // Load value from either limit (numeric) or notes (string)
495
+ existingLimits[tier.id] =
496
+ usageLimit?.freeLimit?.toString() || usageLimit?.notes || "";
497
+ existingPaidLimits[tier.id] = usageLimit?.paidLimit?.toString() || "";
498
+ // Load per-tier overage pricing
499
+ existingOveragePrices[tier.id] = usageLimit?.unitPrice?.toString() || "";
510
500
  if (usageLimit) {
511
501
  enabledTiers.add(tier.id);
512
- if (!existingUnitName && usageLimit.unit) {
513
- existingUnitName = usageLimit.unit;
502
+ // Get unit name from first tier that has it
503
+ if (!existingUnitName && usageLimit.unitName) {
504
+ existingUnitName = usageLimit.unitName;
505
+ }
506
+ // Get reset cycle from first tier that has it
507
+ if (usageLimit.resetCycle) {
508
+ existingResetCycle = usageLimit.resetCycle;
514
509
  }
515
510
  }
516
511
  });
@@ -519,7 +514,7 @@ export function TheMatrix({ document, dispatch }) {
519
514
  setMetricEnabledTiers(enabledTiers);
520
515
  setMetricOveragePrices(existingOveragePrices);
521
516
  setMetricUnitName(existingUnitName);
522
- setMetricResetCycle("MONTHLY");
517
+ setMetricResetCycle(existingResetCycle);
523
518
  };
524
519
  const handleRemoveMetric = (serviceId, metric) => {
525
520
  setPendingRemoveMetric({ serviceId, metric });
@@ -527,13 +522,14 @@ export function TheMatrix({ document, dispatch }) {
527
522
  const confirmRemoveMetric = () => {
528
523
  if (!pendingRemoveMetric)
529
524
  return;
530
- const { metric } = pendingRemoveMetric;
525
+ const { serviceId, metric } = pendingRemoveMetric;
526
+ // Remove this metric from all tiers
531
527
  tiers.forEach((tier) => {
532
- const usageLimit = tier.usageLimits.find((ul) => ul.name === metric);
528
+ const usageLimit = tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === metric);
533
529
  if (usageLimit) {
534
530
  dispatch(removeUsageLimit({
535
531
  tierId: tier.id,
536
- id: usageLimit.id,
532
+ limitId: usageLimit.id,
537
533
  lastModified: new Date().toISOString(),
538
534
  }));
539
535
  }
@@ -573,40 +569,67 @@ export function TheMatrix({ document, dispatch }) {
573
569
  const handleSaveMetric = () => {
574
570
  if (!metricModal || !metricName.trim())
575
571
  return;
576
- const { metric: originalMetric } = metricModal;
572
+ const { serviceId, metric: originalMetric } = metricModal;
577
573
  const now = new Date().toISOString();
578
574
  tiers.forEach((tier) => {
579
575
  const isEnabled = metricEnabledTiers.has(tier.id);
580
576
  const limitValue = metricLimits[tier.id];
581
577
  const existingLimit = originalMetric
582
- ? tier.usageLimits.find((ul) => ul.name === originalMetric)
578
+ ? tier.usageLimits.find((ul) => ul.serviceId === serviceId && ul.metric === originalMetric)
579
+ : null;
580
+ // Check if value is numeric or string
581
+ const parsedLimit = limitValue ? parseInt(limitValue, 10) : null;
582
+ const isNumeric = parsedLimit !== null && !isNaN(parsedLimit);
583
+ // Parse paid limit
584
+ const paidLimitValue = metricPaidLimits[tier.id];
585
+ const parsedPaidLimit = paidLimitValue
586
+ ? parseInt(paidLimitValue, 10)
583
587
  : null;
584
- const parsedLimit = limitValue ? parseInt(limitValue, 10) : 0;
585
- const isNumeric = !isNaN(parsedLimit);
588
+ const isPaidNumeric = parsedPaidLimit !== null && !isNaN(parsedPaidLimit);
589
+ // Get per-tier overage pricing
590
+ const tierOveragePrice = metricOveragePrices[tier.id];
591
+ const parsedOveragePrice = tierOveragePrice
592
+ ? parseFloat(tierOveragePrice)
593
+ : null;
594
+ const hasOveragePricing = parsedOveragePrice !== null && !isNaN(parsedOveragePrice);
586
595
  if (existingLimit && !isEnabled) {
596
+ // Remove limit - tier was disabled
587
597
  dispatch(removeUsageLimit({
588
598
  tierId: tier.id,
589
- id: existingLimit.id,
599
+ limitId: existingLimit.id,
590
600
  lastModified: now,
591
601
  }));
592
602
  }
593
603
  else if (existingLimit && isEnabled) {
604
+ // Update existing limit - use limit for numeric values, notes for strings
594
605
  dispatch(updateUsageLimit({
595
606
  tierId: tier.id,
596
- id: existingLimit.id,
597
- name: metricName.trim(),
598
- limit: isNumeric ? parsedLimit : 0,
599
- unit: metricUnitName.trim() || undefined,
607
+ limitId: existingLimit.id,
608
+ metric: metricName.trim(),
609
+ unitName: metricUnitName.trim() || undefined,
610
+ freeLimit: isNumeric ? parsedLimit : null,
611
+ paidLimit: isPaidNumeric ? parsedPaidLimit : null,
612
+ notes: !isNumeric && limitValue ? limitValue.trim() : null,
613
+ resetCycle: metricResetCycle,
614
+ unitPrice: hasOveragePricing ? parsedOveragePrice : null,
615
+ unitPriceCurrency: hasOveragePricing ? "USD" : undefined,
600
616
  lastModified: now,
601
617
  }));
602
618
  }
603
619
  else if (!existingLimit && isEnabled) {
620
+ // Add new limit - use limit for numeric values, notes for strings
604
621
  dispatch(addUsageLimit({
605
622
  tierId: tier.id,
606
- id: generateId(),
607
- name: metricName.trim(),
608
- limit: isNumeric ? parsedLimit : 0,
609
- unit: metricUnitName.trim() || undefined,
623
+ limitId: generateId(),
624
+ serviceId,
625
+ metric: metricName.trim(),
626
+ unitName: metricUnitName.trim() || undefined,
627
+ freeLimit: isNumeric ? parsedLimit : null,
628
+ paidLimit: isPaidNumeric ? parsedPaidLimit : null,
629
+ notes: !isNumeric && limitValue ? limitValue.trim() : null,
630
+ resetCycle: metricResetCycle,
631
+ unitPrice: hasOveragePricing ? parsedOveragePrice : undefined,
632
+ unitPriceCurrency: hasOveragePricing ? "USD" : undefined,
610
633
  lastModified: now,
611
634
  }));
612
635
  }
@@ -625,6 +648,12 @@ export function TheMatrix({ document, dispatch }) {
625
648
  return { label: "—", color: "#cbd5e1" };
626
649
  const level = serviceLevel.level;
627
650
  const config = SERVICE_LEVELS.find((l) => l.value === level);
651
+ if (level === "CUSTOM" && serviceLevel.customValue) {
652
+ return {
653
+ label: serviceLevel.customValue,
654
+ color: config?.color || "#d97706",
655
+ };
656
+ }
628
657
  return {
629
658
  label: config?.shortLabel || level,
630
659
  color: config?.color || "#475569",
@@ -715,9 +744,9 @@ export function TheMatrix({ document, dispatch }) {
715
744
  costType: null,
716
745
  currency: null,
717
746
  price: null,
718
- pricingMode: "STANDALONE",
747
+ pricingMode: null,
719
748
  standalonePricing: null,
720
- tierDependentPricing: [],
749
+ tierDependentPricing: null,
721
750
  discountMode: null,
722
751
  }, services: ungroupedSetupServices, tiers: tiers, isSetupFormation: true, isOptional: false, isEnabled: true, onToggle: () => { }, getServiceLevelForTier: getServiceLevelForTier, getUniqueMetricsForService: getUniqueMetricsForService, getUsageLimitForMetric: getUsageLimitForMetric, getLevelDisplay: getLevelDisplay, selectedCell: selectedCell, setSelectedCell: setSelectedCell, handleSetServiceLevel: handleSetServiceLevel, dispatch: dispatch, selectedTierIdx: selectedTierIdx, onAddMetric: handleAddMetric, onEditMetric: handleEditMetric, onRemoveMetric: handleRemoveMetric, onEditService: openEditServiceModal, onReorderService: handleReorderService, activeBillingCycle: activeBillingCycle }, "ungrouped-setup")), (regularGroups.length > 0 ||
723
752
  ungroupedRegularServices.length > 0) && (_jsx("tr", { children: _jsxs("td", { colSpan: tiers.length + 1, className: "bg-slate-50 py-3 px-4 text-xs font-semibold text-slate-700 border-b border-slate-200 flex items-center gap-2", children: [_jsx("span", { className: "flex items-center justify-center w-5 h-5 text-slate-500", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", className: "w-full h-full", children: _jsx("path", { d: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" }) }) }), "Recurring Services"] }) })), regularGroups.map((group) => (_jsx(ServiceGroupSection, { group: group, services: groupedServices.get(group.id) || [], tiers: tiers, isSetupFormation: false, isOptional: false, isEnabled: true, onToggle: () => { }, getServiceLevelForTier: getServiceLevelForTier, getUniqueMetricsForService: getUniqueMetricsForService, getUsageLimitForMetric: getUsageLimitForMetric, getLevelDisplay: getLevelDisplay, selectedCell: selectedCell, setSelectedCell: setSelectedCell, handleSetServiceLevel: handleSetServiceLevel, onAddService: openAddServiceModal, selectedTierIdx: selectedTierIdx, dispatch: dispatch, onAddMetric: handleAddMetric, onEditMetric: handleEditMetric, onRemoveMetric: handleRemoveMetric, onEditService: openEditServiceModal, onReorderService: handleReorderService, activeBillingCycle: activeBillingCycle, groupActiveCycle: groupBillingCycles[group.id], onGroupCycleChange: (cycle) => handleGroupCycleChange(group.id, cycle), groupBreakdown: tierBreakdowns[selectedTierIdx]?.optionGroupBreakdowns.find((b) => b.optionGroupId === group.id) }, group.id))), ungroupedRegularServices.length > 0 && (_jsx(ServiceGroupSection, { group: {
@@ -731,19 +760,23 @@ export function TheMatrix({ document, dispatch }) {
731
760
  costType: null,
732
761
  currency: null,
733
762
  price: null,
734
- pricingMode: "STANDALONE",
763
+ pricingMode: null,
735
764
  standalonePricing: null,
736
- tierDependentPricing: [],
765
+ tierDependentPricing: null,
737
766
  discountMode: null,
738
767
  }, services: ungroupedRegularServices, tiers: tiers, isSetupFormation: false, isOptional: false, isEnabled: true, onToggle: () => { }, getServiceLevelForTier: getServiceLevelForTier, getUniqueMetricsForService: getUniqueMetricsForService, getUsageLimitForMetric: getUsageLimitForMetric, getLevelDisplay: getLevelDisplay, selectedCell: selectedCell, setSelectedCell: setSelectedCell, handleSetServiceLevel: handleSetServiceLevel, dispatch: dispatch, selectedTierIdx: selectedTierIdx, onAddMetric: handleAddMetric, onEditMetric: handleEditMetric, onRemoveMetric: handleRemoveMetric, onEditService: openEditServiceModal, onReorderService: handleReorderService, activeBillingCycle: activeBillingCycle }, "ungrouped-regular")), _jsxs("tr", { className: "bg-slate-100 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-100", children: [_jsx("td", { children: "SUBTOTAL" }), tiers.map((tier, idx) => {
739
768
  if (tier.isCustomPricing) {
740
769
  return (_jsx("td", { style: { textAlign: "center" }, children: "Custom" }, tier.id));
741
770
  }
742
771
  const groupSum = tierBreakdowns[idx].tierMonthlyBase;
743
- const tierPrice = tier.pricing?.amount ?? 0;
744
- const currency = tier.pricing?.currency || "USD";
745
- const isOver = tierPrice > 0 && groupSum > tierPrice;
746
- return (_jsx("td", { style: { textAlign: "center" }, children: _jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [_jsx("span", { className: "font-semibold", children: formatPrice(tierPrice > 0 ? tierPrice : groupSum, currency) }), groupSum > 0 &&
772
+ const tierPrice = tier.pricing.amount ?? 0;
773
+ const isCalculated = tier.pricingMode === "CALCULATED";
774
+ const currency = tier.pricing.currency || "USD";
775
+ const isOver = !isCalculated && tierPrice > 0 && groupSum > tierPrice;
776
+ return (_jsx("td", { style: { textAlign: "center" }, children: _jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [_jsx("span", { className: "font-semibold", children: formatPrice(isCalculated ? groupSum : tierPrice, currency) }), isCalculated && (_jsx("span", { className: "inline-block ml-1 px-1 text-[0.5rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle uppercase", title: "Calculated from service groups", style: {
777
+ fontFamily: "'DM Mono', 'SF Mono', monospace",
778
+ }, children: "calc" })), !isCalculated &&
779
+ groupSum > 0 &&
747
780
  tierPrice > 0 &&
748
781
  groupSum !== tierPrice && (_jsx("span", { className: `text-[0.5625rem] py-px px-1.5 rounded-md ${isOver ? "text-rose-700 bg-rose-100 font-semibold" : "text-slate-500 bg-slate-100"}`, style: {
749
782
  fontFamily: "'DM Mono', 'SF Mono', monospace",
@@ -776,11 +809,69 @@ export function TheMatrix({ document, dispatch }) {
776
809
  }, children: idx === selectedTierIdx ? (tier.isCustomPricing ? ("Custom") : (_jsxs(_Fragment, { children: [formatPrice(discountedTotal, breakdown.tierCurrency), savingsPct > 0 && (_jsxs("span", { className: "inline-block ml-1.5 py-px px-1.5 text-[0.5625rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle", style: {
777
810
  fontFamily: "'DM Mono', 'SF Mono', monospace",
778
811
  }, children: ["SAVE ", savingsPct, "%"] }))] }))) : null }, tier.id));
779
- })] })) : (tierBreakdowns[selectedTierIdx]?.optionGroupBreakdowns.map((ogb) => {
780
- const ogGroup = optionGroups.find((g) => g.id === ogb.optionGroupId);
781
- return (_jsxs("tr", { className: "bg-violet-100 [&>td]:py-3.5 [&>td]:px-4 [&>td]:font-bold [&>td]:text-violet-900 [&>td]:border-t-2 [&>td]:border-violet-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-violet-100", children: [_jsxs("td", { children: [ogGroup?.name || ogb.optionGroupId, _jsxs("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: ["/", BILLING_CYCLE_SHORT_LABELS[activeBillingCycle].toLowerCase()] })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx
782
- ? "text-white relative"
783
- : "", style: {
812
+ })] })) : (
813
+ /* Custom billing mode: itemized per-group rows from breakdown */
814
+ tierBreakdowns[selectedTierIdx]?.optionGroupBreakdowns.map((ogb) => (_jsxs("tr", { className: "bg-violet-100 [&>td]:py-3.5 [&>td]:px-4 [&>td]:font-bold [&>td]:text-violet-900 [&>td]:border-t-2 [&>td]:border-violet-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-violet-100", children: [_jsxs("td", { children: [ogb.optionGroupName, _jsxs("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: ["/", BILLING_CYCLE_SHORT_LABELS[ogb.effectiveBillingCycle].toLowerCase()] })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx
815
+ ? "text-white relative"
816
+ : "", style: {
817
+ textAlign: "center",
818
+ ...(idx === selectedTierIdx
819
+ ? {
820
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
821
+ boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
822
+ }
823
+ : {}),
824
+ }, children: idx === selectedTierIdx ? (tier.isCustomPricing ? ("Custom") : ogb.monthlyBase > 0 ? (_jsxs(_Fragment, { children: [formatPrice(ogb.recurringAmount, ogb.currency), ogb.discount &&
825
+ ogb.discount.discountValue > 0 && (_jsxs("span", { className: "inline-block ml-1.5 py-px px-1.5 text-[0.5625rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle", style: {
826
+ fontFamily: "'DM Mono', 'SF Mono', monospace",
827
+ }, children: ["SAVE", " ", Math.round(ogb.discount.discountType ===
828
+ "PERCENTAGE"
829
+ ? ogb.discount.discountValue
830
+ : ogb.cycleAmount > 0
831
+ ? ((ogb.cycleAmount -
832
+ ogb.recurringAmount) /
833
+ ogb.cycleAmount) *
834
+ 100
835
+ : 0), "%"] }))] })) : ("—")) : null }, tier.id)))] }, `group-${ogb.optionGroupId}`)))), tierBreakdowns[selectedTierIdx]?.addOnBreakdowns
836
+ .filter((ab) => ab.monthlyBase > 0)
837
+ .map((ab) => (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsxs("td", { children: ["+ ", ab.optionGroupName, _jsxs("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: ["/", BILLING_CYCLE_SHORT_LABELS[ab.selectedBillingCycle].toLowerCase()] })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
838
+ textAlign: "center",
839
+ ...(idx === selectedTierIdx
840
+ ? {
841
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
842
+ boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
843
+ }
844
+ : {}),
845
+ }, children: idx === selectedTierIdx ? (_jsxs(_Fragment, { children: ["+", formatPrice(ab.recurringAmount, ab.currency), ab.discount && ab.discount.discountValue > 0 && (_jsxs("span", { className: "inline-block ml-1.5 py-px px-1.5 text-[0.5625rem] font-semibold text-emerald-700 bg-emerald-100 rounded-md align-middle", style: {
846
+ fontFamily: "'DM Mono', 'SF Mono', monospace",
847
+ }, children: ["SAVE", " ", Math.round(ab.discount.discountType === "PERCENTAGE"
848
+ ? ab.discount.discountValue
849
+ : ab.cycleAmount > 0
850
+ ? ((ab.cycleAmount -
851
+ ab.recurringAmount) /
852
+ ab.cycleAmount) *
853
+ 100
854
+ : 0), "%"] }))] })) : null }, tier.id)))] }, `addon-recurring-${ab.optionGroupId}`))), tierBreakdowns[selectedTierIdx]?.addOnBreakdowns
855
+ .filter((ab) => ab.setupCost !== null && ab.setupCost > 0)
856
+ .map((ab) => (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsxs("td", { children: ["+ ", ab.optionGroupName, " ", _jsx("span", { className: "font-normal text-[0.6875rem] text-slate-400 ml-1", children: "(one-time setup)" })] }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
857
+ textAlign: "center",
858
+ ...(idx === selectedTierIdx
859
+ ? {
860
+ background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
861
+ boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
862
+ }
863
+ : {}),
864
+ }, children: idx === selectedTierIdx
865
+ ? `${formatPrice(ab.setupCost, ab.setupCostCurrency || "USD")} one-time`
866
+ : null }, tier.id)))] }, `addon-setup-${ab.optionGroupId}`))), (() => {
867
+ const setupBds = tierBreakdowns[selectedTierIdx]?.setupGroupBreakdowns ?? [];
868
+ const totalSetupBase = setupBds.reduce((sum, s) => sum +
869
+ (s.setupCostDiscount?.originalAmount ?? s.setupCost ?? 0), 0);
870
+ const totalSetupEffective = setupBds.reduce((sum, s) => sum + (s.setupCost ?? 0), 0);
871
+ if (totalSetupBase === 0)
872
+ return null;
873
+ const hasDiscount = totalSetupEffective !== totalSetupBase;
874
+ return (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsx("td", { children: "+ Setup & Formation Fees" }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
784
875
  textAlign: "center",
785
876
  ...(idx === selectedTierIdx
786
877
  ? {
@@ -789,23 +880,11 @@ export function TheMatrix({ document, dispatch }) {
789
880
  }
790
881
  : {}),
791
882
  }, children: idx === selectedTierIdx
792
- ? tier.isCustomPricing
793
- ? "Custom"
794
- : ogb.cycleAmount > 0
795
- ? formatPrice(ogb.cycleAmount)
796
- : ""
797
- : null }, tier.id)))] }, `group-${ogb.optionGroupId}`));
798
- })), tierBreakdowns[selectedTierIdx]?.addOnBreakdowns
799
- .filter((ab) => ab.cycleAmount > 0)
800
- .map((ab, abIdx) => (_jsxs("tr", { className: "bg-violet-50 [&>td]:border-t [&>td]:border-dashed [&>td]:border-violet-200 [&>td]:font-semibold [&>td]:text-[0.8125rem] [&>td]:text-violet-700 [&>td]:py-2 [&>td]:px-4 [&>td:first-child]:bg-violet-50", children: [_jsx("td", { children: "+ Add-on" }), tiers.map((tier, idx) => (_jsx("td", { className: idx === selectedTierIdx ? "text-white relative" : "", style: {
801
- textAlign: "center",
802
- ...(idx === selectedTierIdx
803
- ? {
804
- background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)",
805
- boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.15)",
806
- }
807
- : {}),
808
- }, children: idx === selectedTierIdx ? (_jsxs(_Fragment, { children: ["+", formatPrice(ab.cycleAmount)] })) : null }, tier.id)))] }, `addon-recurring-${abIdx}`)))] }) }) })] }), selectedCell && (_jsx(ServiceLevelDetailPanel, { serviceId: selectedCell.serviceId, tierId: selectedCell.tierId, services: services, tiers: tiers, optionGroups: optionGroups, dispatch: dispatch, onClose: () => setSelectedCell(null) })), addServiceModal && (_jsx("div", { className: "fixed inset-0 bg-slate-900/75 backdrop-blur-sm flex items-center justify-center z-[100]", style: { animation: "modal-backdrop 0.2s ease-out" }, children: _jsxs("div", { className: "bg-white rounded-xl p-6 max-h-[85vh] overflow-y-auto", style: {
883
+ ? hasDiscount
884
+ ? `${formatPrice(totalSetupEffective, "USD")} one-time`
885
+ : `${formatPrice(totalSetupBase, "USD")} one-time`
886
+ : null }, tier.id)))] }));
887
+ })()] }) }) })] }), selectedCell && (_jsx(ServiceLevelDetailPanel, { serviceId: selectedCell.serviceId, tierId: selectedCell.tierId, services: services, tiers: tiers, optionGroups: optionGroups, dispatch: dispatch, onClose: () => setSelectedCell(null) })), addServiceModal && (_jsx("div", { className: "fixed inset-0 bg-slate-900/75 backdrop-blur-sm flex items-center justify-center z-[100]", style: { animation: "modal-backdrop 0.2s ease-out" }, children: _jsxs("div", { className: "bg-white rounded-xl p-6 max-h-[85vh] overflow-y-auto", style: {
809
888
  width: "min(32rem, calc(100vw - 2rem))",
810
889
  maxWidth: "32rem",
811
890
  boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.08)",
@@ -821,7 +900,7 @@ export function TheMatrix({ document, dispatch }) {
821
900
  newSet.delete(tier.id);
822
901
  }
823
902
  setNewServiceSelectedTiers(newSet);
824
- }, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing?.amount != null && (_jsxs("span", { className: "text-xs font-semibold text-slate-500 whitespace-nowrap shrink-0", children: ["$", tier.pricing.amount, "/mo"] }))] }, tier.id));
903
+ }, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing.amount !== null && (_jsxs("span", { className: "text-xs font-semibold text-slate-500 whitespace-nowrap shrink-0", children: ["$", tier.pricing.amount, "/mo"] }))] }, tier.id));
825
904
  }) }), newServiceSelectedTiers.size === 0 && (_jsx("p", { className: "text-[0.8125rem] text-slate-500 mt-3 italic", children: "Select at least one tier to include this service" }))] })), _jsxs("p", { className: "text-[0.8125rem] text-slate-600 mb-5 leading-6", children: ["This service will be added to", " ", _jsx("strong", { children: addServiceModal.groupId !== UNGROUPED_ID
826
905
  ? optionGroups.find((g) => g.id === addServiceModal.groupId)
827
906
  ?.name || "Unknown Group"
@@ -848,7 +927,7 @@ export function TheMatrix({ document, dispatch }) {
848
927
  newSet.delete(tier.id);
849
928
  }
850
929
  setEditServiceSelectedTiers(newSet);
851
- }, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing?.amount != null && (_jsxs("span", { className: "text-xs font-semibold text-slate-500 whitespace-nowrap shrink-0", children: ["$", tier.pricing.amount, "/mo"] }))] }, tier.id));
930
+ }, className: "relative w-5 h-5 shrink-0 appearance-none bg-white border-2 border-slate-400 rounded-md cursor-pointer transition-all duration-150 checked:bg-violet-600 checked:border-violet-600" }), _jsx("span", { className: "flex-1 text-sm font-semibold text-slate-800 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: tier.name }), tier.pricing.amount !== null && (_jsxs("span", { className: "text-xs font-semibold text-slate-500 whitespace-nowrap shrink-0", children: ["$", tier.pricing.amount, "/mo"] }))] }, tier.id));
852
931
  }) })] })), _jsxs("div", { className: "flex gap-3 justify-end pt-2", children: [_jsx("button", { onClick: () => {
853
932
  setEditServiceModal(null);
854
933
  setEditServiceName("");
@@ -995,16 +1074,22 @@ function ServiceGroupSection({ group, services, tiers, isSetupFormation, isOptio
995
1074
  (() => {
996
1075
  if (!groupBreakdown)
997
1076
  return null;
998
- const { cycleAmount } = groupBreakdown;
999
- if (cycleAmount <= 0)
1077
+ const { monthlyBase, recurringAmount, discount, currency } = groupBreakdown;
1078
+ if (monthlyBase <= 0 && !group.standalonePricing?.setupCost)
1000
1079
  return null;
1080
+ const setupCost = group.standalonePricing?.setupCost;
1001
1081
  const months = BILLING_CYCLE_MONTHS[effectiveBillingCycle];
1002
1082
  const monthlyEq = months > 0
1003
- ? Math.round((cycleAmount / months) * 100) / 100
1004
- : cycleAmount;
1005
- return (_jsxs("div", { className: "flex items-center gap-3 ml-auto", children: [_jsxs("span", { className: "text-sm font-bold text-slate-800 whitespace-nowrap", children: [formatPrice(effectiveBillingCycle === "MONTHLY"
1006
- ? cycleAmount
1007
- : monthlyEq), "/mo"] }), effectiveBillingCycle !== "MONTHLY" && (_jsxs("span", { className: "text-[0.6875rem] font-medium text-slate-500 whitespace-nowrap", children: ["Billed ", formatPrice(cycleAmount), " ", BILLING_CYCLE_LABELS[effectiveBillingCycle]] }))] }));
1083
+ ? Math.round((recurringAmount / months) * 100) / 100
1084
+ : recurringAmount;
1085
+ const savingsPct = discount && discount.originalAmount > 0
1086
+ ? Math.round(((discount.originalAmount - discount.discountedAmount) /
1087
+ discount.originalAmount) *
1088
+ 100)
1089
+ : 0;
1090
+ return (_jsxs("div", { className: "flex items-center gap-3 ml-auto", children: [monthlyBase > 0 && (_jsxs("span", { className: "text-sm font-bold text-slate-800 whitespace-nowrap", children: [formatPrice(effectiveBillingCycle === "MONTHLY"
1091
+ ? monthlyBase
1092
+ : monthlyEq, currency), "/mo"] })), effectiveBillingCycle !== "MONTHLY" && monthlyBase > 0 && (_jsxs("span", { className: "text-[0.6875rem] font-medium text-slate-500 whitespace-nowrap", children: ["Billed ", formatPrice(recurringAmount, currency), " ", BILLING_CYCLE_LABELS[effectiveBillingCycle]] })), savingsPct > 0 && (_jsxs("span", { className: "text-[0.6875rem] font-semibold text-emerald-600 whitespace-nowrap", children: ["SAVE ", Math.round(savingsPct), "%"] })), setupCost && setupCost.amount > 0 && (_jsxs("span", { className: "text-[0.6875rem] font-medium text-slate-500 whitespace-nowrap", children: ["+", " ", formatPrice(setupCost.amount, setupCost.currency || "USD"), " ", "Setup"] }))] }));
1008
1093
  })()] }) }), _jsx("td", { colSpan: tiers.length, className: headerClass, style: { textAlign: "center" }, children: _jsx("span", { className: `inline-block py-1 px-2.5 rounded-md text-[0.625rem] font-semibold uppercase tracking-[0.04em] ${isSetupFormation || !isOptional
1009
1094
  ? "bg-emerald-100 text-emerald-700"
1010
1095
  : "bg-sky-200 text-sky-700"}`, children: isSetupFormation
@@ -1021,20 +1106,53 @@ function ServiceGroupSection({ group, services, tiers, isSetupFormation, isOptio
1021
1106
  return (_jsxs("tr", { className: "bg-slate-50 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-200 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-50", children: [_jsx("td", { children: "TOTAL SETUP FEE" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: "No setup fee configured" })] }));
1022
1107
  }
1023
1108
  const selectedTier = tiers[selectedTierIdx] ?? null;
1109
+ const tierPricing = selectedTier
1110
+ ? group.tierDependentPricing?.find((tp) => tp.tierId === selectedTier.id)
1111
+ : null;
1112
+ const cycleDiscount = tierPricing?.setupCostDiscounts?.find((d) => d.billingCycle === activeBillingCycle);
1113
+ const genericDiscount = tierPricing?.setupCost?.discount;
1114
+ const discount = cycleDiscount?.discountRule ?? genericDiscount;
1115
+ let effectivePrice = basePrice;
1116
+ if (discount && discount.discountValue > 0) {
1117
+ if (discount.discountType === "PERCENTAGE") {
1118
+ effectivePrice = basePrice * (1 - discount.discountValue / 100);
1119
+ }
1120
+ else {
1121
+ effectivePrice = Math.max(0, basePrice - discount.discountValue);
1122
+ }
1123
+ effectivePrice = Math.round(effectivePrice * 100) / 100;
1124
+ }
1024
1125
  const curr = group.currency || "USD";
1025
- return (_jsxs("tr", { className: "bg-slate-50 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-200 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-50", children: [_jsx("td", { children: "TOTAL SETUP FEE" }), _jsxs("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: [formatPrice(basePrice, curr), " flat fee"] })] }));
1126
+ const hasDiscount = effectivePrice !== basePrice;
1127
+ return (_jsxs("tr", { className: "bg-slate-50 [&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-200 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 [&>td:first-child]:bg-slate-50", children: [_jsx("td", { children: "TOTAL SETUP FEE" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: hasDiscount ? (_jsxs(_Fragment, { children: [_jsx("span", { style: {
1128
+ textDecoration: "line-through",
1129
+ opacity: 0.5,
1130
+ marginRight: 6,
1131
+ }, children: formatPrice(basePrice, curr) }), formatPrice(effectivePrice, curr), " flat fee", discount?.discountType === "PERCENTAGE"
1132
+ ? ` (${discount.discountValue}% off)`
1133
+ : ` (${formatPrice(discount?.discountValue ?? 0, curr)} off)`] })) : (`${formatPrice(basePrice, curr)} flat fee (applied to all ${tiers.some((t) => {
1134
+ const tp = group.tierDependentPricing?.find((p) => p.tierId === t.id);
1135
+ const monthlyAmt = tp?.recurringPricing?.find((r) => r.billingCycle === "MONTHLY")?.amount;
1136
+ return !monthlyAmt || monthlyAmt === 0;
1137
+ })
1138
+ ? "priced "
1139
+ : ""}tiers)`) })] }));
1026
1140
  })(), isOptional &&
1027
1141
  (() => {
1028
- const cycleAmount = isEnabled
1029
- ? (groupBreakdown?.cycleAmount ?? 0)
1142
+ const baseMonthly = isEnabled
1143
+ ? (groupBreakdown?.monthlyBase ?? 0)
1144
+ : 0;
1145
+ const adjustedTotal = isEnabled
1146
+ ? (groupBreakdown?.recurringAmount ?? 0)
1147
+ : 0;
1148
+ const setupCost = isEnabled
1149
+ ? (group.standalonePricing?.setupCost?.amount ?? 0)
1030
1150
  : 0;
1031
1151
  const billingLabel = `/${BILLING_CYCLE_SHORT_LABELS[effectiveBillingCycle].toLowerCase()}`;
1032
- const currency = group.currency || "USD";
1033
- return (_jsxs("tr", { className: `[&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 ${headerClass}`, children: [_jsx("td", { className: headerClass, children: "SUBTOTAL" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: isEnabled && cycleAmount > 0
1034
- ? `+${formatPrice(cycleAmount, currency)}${billingLabel}`
1035
- : isEnabled
1036
- ? "Included"
1037
- : "—" })] }));
1152
+ const currency = groupBreakdown?.currency || group.currency || "USD";
1153
+ return (_jsxs("tr", { className: `[&>td]:py-2.5 [&>td]:px-4 [&>td]:font-semibold [&>td]:text-slate-700 [&>td]:border-b [&>td]:border-slate-300 [&>td:first-child]:sticky [&>td:first-child]:left-0 [&>td:first-child]:z-10 ${headerClass}`, children: [_jsx("td", { className: headerClass, children: "SUBTOTAL" }), _jsx("td", { colSpan: tiers.length, style: { textAlign: "center" }, children: isEnabled && (baseMonthly > 0 || setupCost > 0) ? (_jsxs(_Fragment, { children: [baseMonthly > 0 &&
1154
+ `+${formatPrice(adjustedTotal, currency)}${billingLabel}`, baseMonthly > 0 && setupCost > 0 && " + ", setupCost > 0 &&
1155
+ `${formatPrice(setupCost, currency)} setup`] })) : isEnabled ? ("Included") : ("—") })] }));
1038
1156
  })()] }));
1039
1157
  }
1040
1158
  function ServiceRowWithMetrics({ service, metrics, tiers, rowClass, getServiceLevelForTier, getUsageLimitForMetric, getLevelDisplay, selectedCell, setSelectedCell, selectedTierIdx, onAddMetric, onEditMetric, onRemoveMetric, onEditService, onReorderService, groupServices, serviceIndex, }) {
@@ -1095,7 +1213,10 @@ function ServiceRowWithMetrics({ service, metrics, tiers, rowClass, getServiceLe
1095
1213
  ? {
1096
1214
  background: "linear-gradient(180deg, rgba(139, 92, 246, 0.06) 0%, rgba(139, 92, 246, 0.12) 100%)",
1097
1215
  }
1098
- : undefined, children: _jsx("div", { className: "inline-flex flex-col border border-slate-200 rounded-[10px] overflow-hidden min-w-[10rem]", children: usageLimit ? (_jsxs("div", { className: "flex justify-between items-center py-1.5 px-3 gap-4", children: [_jsx("span", { className: "text-[0.6875rem] text-slate-500 whitespace-nowrap", children: "Limit" }), _jsx("span", { className: "text-xs text-slate-700 text-right whitespace-nowrap", children: _jsxs("strong", { children: [usageLimit.limit, usageLimit.unit ? ` ${usageLimit.unit}` : ""] }) })] })) : (_jsx("span", { className: "text-xs text-slate-300", children: "\u2014" })) }) }, tier.id));
1216
+ : undefined, children: _jsx("div", { className: "inline-flex flex-col border border-slate-200 rounded-[10px] overflow-hidden min-w-[10rem]", children: usageLimit ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex justify-between items-center py-1.5 px-3 gap-4", children: [_jsx("span", { className: "text-[0.6875rem] text-slate-500 whitespace-nowrap", children: "Included" }), _jsx("span", { className: "text-xs text-slate-700 text-right whitespace-nowrap", children: usageLimit.freeLimit != null ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [usageLimit.freeLimit, usageLimit.unitName
1217
+ ? ` ${usageLimit.unitName}`
1218
+ : ""] }), usageLimit.resetCycle &&
1219
+ usageLimit.resetCycle !== "NONE" && (_jsxs("span", { className: "text-[0.625rem] font-normal text-slate-400", children: [" ", "/ ", usageLimit.resetCycle.toLowerCase()] }))] })) : (_jsx("strong", { children: usageLimit.notes || "Unlimited" })) })] }), usageLimit.unitPrice != null && (_jsxs("div", { className: "flex justify-between items-center py-1.5 px-3 gap-4 border-t border-slate-100", children: [_jsx("span", { className: "text-[0.6875rem] text-slate-500 whitespace-nowrap", children: "Overage" }), _jsxs("span", { className: "text-xs text-emerald-600 font-medium text-right whitespace-nowrap", children: [formatPrice(usageLimit.unitPrice, usageLimit.unitPriceCurrency || "USD"), _jsxs("span", { className: "text-[0.625rem] font-normal text-slate-400", children: [" ", "/ extra"] })] })] }))] })) : (_jsx("span", { className: "text-xs text-slate-300", children: "\u2014" })) }) }, tier.id));
1099
1220
  })] }, `${service.id}-${metric}`)))] }));
1100
1221
  }
1101
1222
  function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGroups: _optionGroups, dispatch, onClose, }) {
@@ -1161,21 +1282,28 @@ function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGro
1161
1282
  const serviceLevel = service
1162
1283
  ? tier?.serviceLevels.find((sl) => sl.serviceId === serviceId)
1163
1284
  : undefined;
1164
- const usageLimits = service ? tier?.usageLimits || [] : [];
1285
+ const usageLimits = service
1286
+ ? tier?.usageLimits.filter((ul) => ul.serviceId === serviceId) || []
1287
+ : [];
1165
1288
  const [isAddingMetric, setIsAddingMetric] = useState(false);
1166
1289
  const [newMetric, setNewMetric] = useState("");
1167
1290
  const [newLimit, setNewLimit] = useState("");
1291
+ const [customValue, setCustomValue] = useState(serviceLevel?.customValue || "");
1168
1292
  if (!service || !tier)
1169
1293
  return null;
1170
1294
  const handleAddLimit = () => {
1171
1295
  if (!newMetric.trim())
1172
1296
  return;
1173
- const parsedLimit = newLimit ? parseInt(newLimit, 10) : NaN;
1297
+ const parsedLimit = newLimit ? parseInt(newLimit, 10) : null;
1298
+ const isNumeric = parsedLimit !== null && !isNaN(parsedLimit);
1174
1299
  dispatch(addUsageLimit({
1175
1300
  tierId: tier.id,
1176
- id: generateId(),
1177
- name: newMetric.trim(),
1178
- limit: !isNaN(parsedLimit) ? parsedLimit : 0,
1301
+ limitId: generateId(),
1302
+ serviceId: service.id,
1303
+ metric: newMetric.trim(),
1304
+ freeLimit: isNumeric ? parsedLimit : undefined,
1305
+ notes: !isNumeric && newLimit ? newLimit.trim() : undefined,
1306
+ resetCycle: "MONTHLY",
1179
1307
  lastModified: new Date().toISOString(),
1180
1308
  }));
1181
1309
  setNewMetric("");
@@ -1185,7 +1313,7 @@ function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGro
1185
1313
  const handleRemoveLimit = (limitId) => {
1186
1314
  dispatch(removeUsageLimit({
1187
1315
  tierId: tier.id,
1188
- id: limitId,
1316
+ limitId,
1189
1317
  lastModified: new Date().toISOString(),
1190
1318
  }));
1191
1319
  };
@@ -1200,29 +1328,56 @@ function ServiceLevelDetailPanel({ serviceId, tierId, services, tiers, optionGro
1200
1328
  }
1201
1329
  function MetricLimitItem({ limit, tierId, dispatch, onRemove, }) {
1202
1330
  const [isEditing, setIsEditing] = useState(false);
1203
- const [editMetric, setEditMetric] = useState(limit.name);
1204
- const [editUnit, setEditUnit] = useState(limit.unit || "");
1205
- const [editLimit, setEditLimit] = useState(limit.limit?.toString() || "");
1331
+ const [editMetric, setEditMetric] = useState(limit.metric);
1332
+ const [editUnitName, setEditUnitName] = useState(limit.unitName || "");
1333
+ const [editLimit, setEditLimit] = useState(limit.freeLimit?.toString() || limit.notes || "");
1334
+ const [editPaidLimit, setEditPaidLimit] = useState(limit.paidLimit?.toString() || "");
1335
+ const [editResetCycle, setEditResetCycle] = useState(limit.resetCycle || "MONTHLY");
1336
+ // Overage pricing state
1337
+ const [editUnitPrice, setEditUnitPrice] = useState(limit.unitPrice?.toString() || "");
1338
+ const [editUnitPriceCurrency] = useState(limit.unitPriceCurrency || "USD");
1206
1339
  const handleSave = () => {
1207
1340
  const parsedLimit = editLimit ? parseInt(editLimit, 10) : null;
1341
+ const isNumeric = parsedLimit !== null && !isNaN(parsedLimit);
1342
+ const parsedPaidLimit = editPaidLimit ? parseInt(editPaidLimit, 10) : null;
1343
+ const isPaidNumeric = parsedPaidLimit !== null && !isNaN(parsedPaidLimit);
1344
+ const parsedUnitPrice = editUnitPrice ? parseFloat(editUnitPrice) : null;
1208
1345
  dispatch(updateUsageLimit({
1209
1346
  tierId,
1210
- id: limit.id,
1211
- name: editMetric.trim() || limit.name,
1212
- unit: editUnit.trim() || undefined,
1213
- limit: parsedLimit !== null && !isNaN(parsedLimit) ? parsedLimit : undefined,
1347
+ limitId: limit.id,
1348
+ metric: editMetric.trim() || limit.metric,
1349
+ unitName: editUnitName.trim() || undefined,
1350
+ freeLimit: isNumeric ? parsedLimit : undefined,
1351
+ paidLimit: isPaidNumeric ? parsedPaidLimit : undefined,
1352
+ notes: !isNumeric && editLimit ? editLimit.trim() : undefined,
1353
+ resetCycle: editResetCycle,
1354
+ unitPrice: parsedUnitPrice,
1355
+ unitPriceCurrency: parsedUnitPrice ? editUnitPriceCurrency : undefined,
1214
1356
  lastModified: new Date().toISOString(),
1215
1357
  }));
1216
1358
  setIsEditing(false);
1217
1359
  };
1218
1360
  const handleCancel = () => {
1219
- setEditMetric(limit.name);
1220
- setEditUnit(limit.unit || "");
1221
- setEditLimit(limit.limit?.toString() || "");
1361
+ setEditMetric(limit.metric);
1362
+ setEditUnitName(limit.unitName || "");
1363
+ setEditLimit(limit.freeLimit?.toString() || limit.notes || "");
1364
+ setEditPaidLimit(limit.paidLimit?.toString() || "");
1365
+ setEditResetCycle(limit.resetCycle || "MONTHLY");
1366
+ setEditUnitPrice(limit.unitPrice?.toString() || "");
1222
1367
  setIsEditing(false);
1223
1368
  };
1369
+ // Format overage display string
1370
+ const getOverageDisplay = () => {
1371
+ if (!limit.unitPrice)
1372
+ return null;
1373
+ const unitLabel = limit.unitName || "unit";
1374
+ return `+${formatPrice(limit.unitPrice, limit.unitPriceCurrency || "USD")} per ${unitLabel}`;
1375
+ };
1376
+ const overageDisplay = getOverageDisplay();
1224
1377
  if (isEditing) {
1225
- return (_jsxs("div", { className: "p-3 bg-violet-50 rounded-[10px] mb-3 [&>div]:mb-2.5 [&>div:last-child]:mb-0", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Metric Name" }), _jsx("input", { type: "text", value: editMetric, onChange: (e) => setEditMetric(e.target.value), placeholder: "e.g., Number of Entities", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Unit" }), _jsx("input", { type: "text", value: editUnit, onChange: (e) => setEditUnit(e.target.value), placeholder: "e.g., entity, credit card, contractor", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Limit" }), _jsx("input", { type: "text", value: editLimit, onChange: (e) => setEditLimit(e.target.value), placeholder: "e.g., 100", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: handleSave, className: "flex-1 py-2 px-3 text-[0.8125rem] font-semibold rounded-[10px] cursor-pointer transition-all duration-150 bg-violet-600 text-white border-none hover:enabled:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: "Save" }), _jsx("button", { onClick: handleCancel, className: "flex-1 py-2 px-3 text-[0.8125rem] font-semibold rounded-[10px] cursor-pointer transition-all duration-150 bg-slate-200 text-slate-700 border-none hover:bg-slate-300", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: "Cancel" })] })] }));
1378
+ return (_jsxs("div", { className: "p-3 bg-violet-50 rounded-[10px] mb-3 [&>div]:mb-2.5 [&>div:last-child]:mb-0", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Metric Name" }), _jsx("input", { type: "text", value: editMetric, onChange: (e) => setEditMetric(e.target.value), placeholder: "e.g., Number of Entities", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, autoFocus: true })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Unit Name" }), _jsx("input", { type: "text", value: editUnitName, onChange: (e) => setEditUnitName(e.target.value), placeholder: "e.g., entity, credit card, contractor", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", children: "Used for overage pricing display (e.g., \"$50 per entity\")" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Free Limit" }), _jsx("input", { type: "text", value: editLimit, onChange: (e) => setEditLimit(e.target.value), placeholder: "e.g., 100, Unlimited, Custom", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", children: "Included free limit for this tier" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Paid Limit" }), _jsx("input", { type: "text", value: editPaidLimit, onChange: (e) => setEditPaidLimit(e.target.value), placeholder: "e.g., 500, 1000", className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)]", style: { fontFamily: "'DM Sans', system-ui, sans-serif" } }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", children: "Maximum paid usage beyond the free limit (optional)" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Reset Cycle" }), _jsxs("select", { value: editResetCycle, onChange: (e) => setEditResetCycle(e.target.value), className: "w-full text-[0.8125rem] text-slate-900 bg-white border-[1.5px] border-slate-300 rounded-[10px] py-2.5 px-3.5 outline-none transition-all duration-150 focus:border-violet-500 focus:shadow-[0_0_0_3px_rgba(139,92,246,0.15)] cursor-pointer", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: [_jsx("option", { value: "NONE", children: "None (One-time)" }), _jsx("option", { value: "DAILY", children: "Daily" }), _jsx("option", { value: "WEEKLY", children: "Weekly" }), _jsx("option", { value: "MONTHLY", children: "Monthly" })] })] }), _jsxs("div", { className: "mt-2 pt-3 border-t border-dashed border-slate-300", children: [_jsx("label", { className: "block text-[0.625rem] font-semibold uppercase tracking-[0.08em] text-slate-500 mb-1", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "Overage Pricing (Optional)" }), _jsx("p", { className: "text-[0.6875rem] text-slate-400 mt-1", style: { marginBottom: "0.5rem" }, children: "Set a price for usage beyond the included limit" }), _jsxs("div", { className: "flex items-center gap-1.5 flex-wrap", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("span", { className: "text-sm text-slate-500", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: "$" }), _jsx("input", { type: "number", value: editUnitPrice, onChange: (e) => setEditUnitPrice(e.target.value), placeholder: "0.00", step: "0.01", className: "w-[4.5rem] text-sm font-medium text-slate-900 bg-white border border-slate-300 rounded-md py-1.5 px-2 outline-none transition-colors duration-150 focus:border-violet-600", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" } })] }), _jsxs("span", { className: "text-xs text-slate-500", children: ["per ", editUnitName || "unit"] })] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: handleSave, className: "flex-1 py-2 px-3 text-[0.8125rem] font-semibold rounded-[10px] cursor-pointer transition-all duration-150 bg-violet-600 text-white border-none hover:enabled:bg-violet-700 disabled:opacity-50 disabled:cursor-not-allowed", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: "Save" }), _jsx("button", { onClick: handleCancel, className: "flex-1 py-2 px-3 text-[0.8125rem] font-semibold rounded-[10px] cursor-pointer transition-all duration-150 bg-slate-200 text-slate-700 border-none hover:bg-slate-300", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: "Cancel" })] })] }));
1226
1379
  }
1227
- return (_jsxs("div", { className: "group/limititem flex items-center gap-3 p-3 bg-slate-50 border border-slate-200 rounded-[10px] mb-3", children: [_jsxs("div", { className: "flex-1 cursor-pointer p-1 -m-1 rounded-md transition-all duration-150 hover:bg-slate-200", onClick: () => setIsEditing(true), children: [_jsx("div", { className: "text-sm font-semibold text-slate-900", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: limit.name }), _jsx("div", { className: "flex flex-col gap-0.5", children: _jsxs("div", { className: "text-[0.8125rem] text-slate-500", children: [limit.limit != null ? `Limit: ${limit.limit}` : "—", limit.unit ? ` ${limit.unit}` : ""] }) })] }), _jsxs("div", { className: "flex gap-1 opacity-0 group-hover/limititem:opacity-100 transition-all duration-150", children: [_jsx("button", { onClick: () => setIsEditing(true), className: "p-1 bg-transparent border-none text-slate-400 cursor-pointer rounded-md transition-all duration-150 hover:bg-slate-200 hover:text-violet-600", title: "Edit metric", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" }) }) }), _jsx("button", { onClick: onRemove, className: "p-1 bg-transparent border-none text-slate-400 cursor-pointer rounded-md transition-all duration-150 hover:bg-slate-200 hover:text-rose-600", title: "Remove metric", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] })] }));
1380
+ return (_jsxs("div", { className: "group/limititem flex items-center gap-3 p-3 bg-slate-50 border border-slate-200 rounded-[10px] mb-3", children: [_jsxs("div", { className: "flex-1 cursor-pointer p-1 -m-1 rounded-md transition-all duration-150 hover:bg-slate-200", onClick: () => setIsEditing(true), children: [_jsx("div", { className: "text-sm font-semibold text-slate-900", style: { fontFamily: "'DM Sans', system-ui, sans-serif" }, children: limit.metric }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("div", { className: "text-[0.8125rem] text-slate-500", children: limit.freeLimit != null
1381
+ ? `Free: ${limit.freeLimit}${limit.paidLimit != null ? ` / Paid: ${limit.paidLimit}` : ""}`
1382
+ : (limit.notes ?? "—") }), limit.resetCycle && (_jsxs("div", { style: { fontSize: "0.6875rem", color: "#64748b" }, children: ["Resets ", limit.resetCycle.toLowerCase()] })), overageDisplay && (_jsx("div", { className: "text-[0.6875rem] text-emerald-600 font-medium", style: { fontFamily: "'DM Mono', 'SF Mono', monospace" }, children: overageDisplay }))] })] }), _jsxs("div", { className: "flex gap-1 opacity-0 group-hover/limititem:opacity-100 transition-all duration-150", children: [_jsx("button", { onClick: () => setIsEditing(true), className: "p-1 bg-transparent border-none text-slate-400 cursor-pointer rounded-md transition-all duration-150 hover:bg-slate-200 hover:text-violet-600", title: "Edit metric", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" }) }) }), _jsx("button", { onClick: onRemove, className: "p-1 bg-transparent border-none text-slate-400 cursor-pointer rounded-md transition-all duration-150 hover:bg-slate-200 hover:text-rose-600", title: "Remove metric", children: _jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] })] }));
1228
1383
  }