@powerhousedao/service-offering 0.0.6 → 0.0.7

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.
@@ -19,12 +19,12 @@ export interface MapOfferingOptions {
19
19
  * This is a one-time snapshot — the SI lives independently after creation.
20
20
  *
21
21
  * Logic:
22
- * 1. Find the selected tier
23
- * 2. For each service group, find the tier-specific pricing for the selected billing cycle
24
- * 3. Apply billing cycle discounts if any
25
- * 4. Map services based on service level bindings (INCLUDED, OPTIONAL, CUSTOM, VARIABLE)
26
- * 5. Map usage limits to metrics with freeLimit/paidLimit
27
- * 6. Calculate tier price from service group sums (CALCULATED mode) or use manual price
22
+ * 1. Find the selected tier and resolve pricing from finalConfiguration
23
+ * 2. Map offering service groups with tier-specific pricing
24
+ * 3. Map option group configs from finalConfiguration as additional service groups
25
+ * 4. Map add-on configs from finalConfiguration as optional service groups
26
+ * 5. Map remaining standalone services with tier service levels and usage limits
27
+ * 6. Calculate tier price from finalConfig or service group sums (CALCULATED) or manual price
28
28
  */
29
29
  export declare function mapOfferingToSubscription(options: MapOfferingOptions): InitializeSubscriptionInput;
30
30
  //# sourceMappingURL=mapOfferingToSubscription.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mapOfferingToSubscription.d.ts","sourceRoot":"","sources":["../../../../editors/subscription-instance-editor/components/mapOfferingToSubscription.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EAOrB,MAAM,+DAA+D,CAAC;AACvE,OAAO,KAAK,EACV,2BAA2B,EAK3B,YAAY,IAAI,cAAc,EAC/B,MAAM,oEAAoE,CAAC;AAE5E,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,oBAAoB,EAAE,cAAc,CAAC;IACrC,oBAAoB;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,GAC1B,2BAA2B,CA6D7B"}
1
+ {"version":3,"file":"mapOfferingToSubscription.d.ts","sourceRoot":"","sources":["../../../../editors/subscription-instance-editor/components/mapOfferingToSubscription.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,oBAAoB,EAMrB,MAAM,+DAA+D,CAAC;AACvE,OAAO,KAAK,EACV,2BAA2B,EAK3B,YAAY,IAAI,cAAc,EAC/B,MAAM,oEAAoE,CAAC;AAE5E,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,oBAAoB,EAAE,cAAc,CAAC;IACrC,oBAAoB;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,GAC1B,2BAA2B,CAqF7B"}
@@ -4,12 +4,12 @@ import { generateId } from "document-model/core";
4
4
  * This is a one-time snapshot — the SI lives independently after creation.
5
5
  *
6
6
  * Logic:
7
- * 1. Find the selected tier
8
- * 2. For each service group, find the tier-specific pricing for the selected billing cycle
9
- * 3. Apply billing cycle discounts if any
10
- * 4. Map services based on service level bindings (INCLUDED, OPTIONAL, CUSTOM, VARIABLE)
11
- * 5. Map usage limits to metrics with freeLimit/paidLimit
12
- * 6. Calculate tier price from service group sums (CALCULATED mode) or use manual price
7
+ * 1. Find the selected tier and resolve pricing from finalConfiguration
8
+ * 2. Map offering service groups with tier-specific pricing
9
+ * 3. Map option group configs from finalConfiguration as additional service groups
10
+ * 4. Map add-on configs from finalConfiguration as optional service groups
11
+ * 5. Map remaining standalone services with tier service levels and usage limits
12
+ * 6. Calculate tier price from finalConfig or service group sums (CALCULATED) or manual price
13
13
  */
14
14
  export function mapOfferingToSubscription(options) {
15
15
  const { offering, tierId, selectedBillingCycle, customerId, customerName, customerEmail, createdAt, } = options;
@@ -17,21 +17,34 @@ export function mapOfferingToSubscription(options) {
17
17
  if (!tier) {
18
18
  throw new Error(`Tier ${tierId} not found in offering`);
19
19
  }
20
- const currency = tier.pricing.currency;
20
+ const finalConfig = offering.finalConfiguration;
21
+ const currency = finalConfig?.tierCurrency ?? tier.pricing.currency;
21
22
  const pricingMode = tier.pricingMode || "MANUAL_OVERRIDE";
22
- // Map service groups with their tier-specific pricing
23
- const serviceGroups = mapServiceGroups(offering.serviceGroups, tier, selectedBillingCycle, currency);
24
- // Map standalone services (not in any group) based on tier service levels
25
- const standaloneServices = mapStandaloneServices(offering.services, tier, currency);
23
+ // Track which services are accounted for in groups
24
+ const groupedServiceIds = new Set();
25
+ // 1. Map offering service groups with tier-specific pricing
26
+ const serviceGroups = mapOfferingServiceGroups(offering, tier, selectedBillingCycle, currency, groupedServiceIds);
27
+ // 2. Map option group configs from finalConfiguration as service groups
28
+ if (finalConfig) {
29
+ mapFinalConfigGroups(offering, tier, finalConfig, currency, groupedServiceIds, serviceGroups);
30
+ }
31
+ // 3. Map remaining standalone services (not in any group or option group)
32
+ const standaloneServices = offering.services
33
+ .filter((s) => !groupedServiceIds.has(s.id))
34
+ .filter((svc) => {
35
+ const level = tier.serviceLevels.find((sl) => sl.serviceId === svc.id);
36
+ return (level &&
37
+ level.level !== "NOT_INCLUDED" &&
38
+ level.level !== "NOT_APPLICABLE");
39
+ })
40
+ .map((svc) => mapServiceToInput(svc, tier, currency, selectedBillingCycle));
26
41
  // Calculate tier price
27
42
  let tierPrice;
28
43
  if (pricingMode === "CALCULATED") {
29
- tierPrice = serviceGroups.reduce((sum, grp) => {
30
- return sum + (grp.recurringAmount ?? 0);
31
- }, 0);
44
+ tierPrice = serviceGroups.reduce((sum, grp) => sum + (grp.recurringAmount ?? 0), 0);
32
45
  }
33
46
  else {
34
- tierPrice = tier.pricing.amount ?? undefined;
47
+ tierPrice = finalConfig?.tierBasePrice ?? tier.pricing.amount ?? undefined;
35
48
  }
36
49
  return {
37
50
  customerId: customerId ?? undefined,
@@ -51,18 +64,21 @@ export function mapOfferingToSubscription(options) {
51
64
  serviceGroups,
52
65
  };
53
66
  }
54
- function mapServiceGroups(soGroups, tier, selectedBillingCycle, globalCurrency) {
55
- return soGroups.map((group) => {
67
+ /**
68
+ * Maps offering service groups to subscription service groups.
69
+ * Finds services by their serviceGroupId and applies tier-specific pricing.
70
+ */
71
+ function mapOfferingServiceGroups(offering, tier, selectedBillingCycle, globalCurrency, groupedServiceIds) {
72
+ return offering.serviceGroups.map((group) => {
73
+ // Find services that belong to this service group
74
+ const groupServices = offering.services.filter((s) => s.serviceGroupId === group.id);
75
+ groupServices.forEach((s) => groupedServiceIds.add(s.id));
56
76
  // Find tier-specific pricing for this group
57
77
  const tierPricing = group.tierPricing.find((tp) => tp.tierId === tier.id);
58
78
  // Find the recurring price option matching the selected billing cycle
59
- let recurringOption;
60
- if (tierPricing) {
61
- recurringOption = tierPricing.recurringPricing.find((rp) => rp.billingCycle === selectedBillingCycle);
62
- // Fallback to group's own billing cycle if no match
63
- if (!recurringOption) {
64
- recurringOption = tierPricing.recurringPricing.find((rp) => rp.billingCycle === group.billingCycle);
65
- }
79
+ let recurringOption = tierPricing?.recurringPricing.find((rp) => rp.billingCycle === selectedBillingCycle);
80
+ if (!recurringOption) {
81
+ recurringOption = tierPricing?.recurringPricing.find((rp) => rp.billingCycle === group.billingCycle);
66
82
  }
67
83
  // Apply billing cycle discount from tier if applicable
68
84
  let discountedAmount = recurringOption?.amount;
@@ -72,12 +88,10 @@ function mapServiceGroups(soGroups, tier, selectedBillingCycle, globalCurrency)
72
88
  if (cycleDiscount) {
73
89
  const originalAmount = recurringOption.amount;
74
90
  const rule = cycleDiscount.discountRule;
75
- if (rule.discountType === "PERCENTAGE") {
76
- discountedAmount = originalAmount * (1 - rule.discountValue / 100);
77
- }
78
- else {
79
- discountedAmount = originalAmount - rule.discountValue;
80
- }
91
+ discountedAmount =
92
+ rule.discountType === "PERCENTAGE"
93
+ ? originalAmount * (1 - rule.discountValue / 100)
94
+ : originalAmount - rule.discountValue;
81
95
  discountInput = {
82
96
  originalAmount,
83
97
  discountType: rule.discountType,
@@ -86,16 +100,14 @@ function mapServiceGroups(soGroups, tier, selectedBillingCycle, globalCurrency)
86
100
  };
87
101
  }
88
102
  }
89
- // Also check if the recurring option itself has a discount
103
+ // Fallback to the recurring option's own discount
90
104
  if (recurringOption?.discount && !discountInput) {
91
105
  const originalAmount = recurringOption.amount;
92
106
  const d = recurringOption.discount;
93
- if (d.discountType === "PERCENTAGE") {
94
- discountedAmount = originalAmount * (1 - d.discountValue / 100);
95
- }
96
- else {
97
- discountedAmount = originalAmount - d.discountValue;
98
- }
107
+ discountedAmount =
108
+ d.discountType === "PERCENTAGE"
109
+ ? originalAmount * (1 - d.discountValue / 100)
110
+ : originalAmount - d.discountValue;
99
111
  discountInput = {
100
112
  originalAmount,
101
113
  discountType: d.discountType,
@@ -113,8 +125,15 @@ function mapServiceGroups(soGroups, tier, selectedBillingCycle, globalCurrency)
113
125
  setupCurrency = setupCostOption.currency;
114
126
  }
115
127
  }
116
- // Map services in this group based on tier service levels
117
- const groupServices = mapGroupServices(group.id, tier, globalCurrency);
128
+ // Map services in this group
129
+ const mappedServices = groupServices
130
+ .filter((svc) => {
131
+ const level = tier.serviceLevels.find((sl) => sl.serviceId === svc.id);
132
+ return (!level ||
133
+ (level.level !== "NOT_INCLUDED" && level.level !== "NOT_APPLICABLE"));
134
+ })
135
+ .map((svc) => mapServiceToInput(svc, tier, globalCurrency, (recurringOption?.billingCycle ??
136
+ group.billingCycle)));
118
137
  return {
119
138
  id: generateId(),
120
139
  name: group.name,
@@ -127,54 +146,100 @@ function mapServiceGroups(soGroups, tier, selectedBillingCycle, globalCurrency)
127
146
  recurringBillingCycle: (recurringOption?.billingCycle ??
128
147
  group.billingCycle),
129
148
  recurringDiscount: discountInput,
130
- services: groupServices,
149
+ services: mappedServices,
131
150
  };
132
151
  });
133
152
  }
134
- function mapGroupServices(groupId, tier, globalCurrency) {
135
- // Find services that belong to this group via tier service levels
136
- const relevantLevels = tier.serviceLevels.filter((sl) => {
137
- // Include services that are INCLUDED, OPTIONAL, CUSTOM, or VARIABLE for this tier
138
- return (sl.level === "INCLUDED" ||
139
- sl.level === "OPTIONAL" ||
140
- sl.level === "CUSTOM" ||
141
- sl.level === "VARIABLE");
142
- });
143
- return relevantLevels.map((sl) => {
144
- // Map usage limits for this service
145
- const metrics = mapUsageLimits(sl.serviceId, tier.usageLimits, globalCurrency);
146
- return {
153
+ /**
154
+ * Maps finalConfiguration option group configs and add-on configs
155
+ * into subscription service groups.
156
+ */
157
+ function mapFinalConfigGroups(offering, tier, finalConfig, globalCurrency, groupedServiceIds, serviceGroups) {
158
+ // Non-add-on option groups
159
+ for (const ogConfig of finalConfig.optionGroupConfigs) {
160
+ const og = offering.optionGroups.find((g) => g.id === ogConfig.optionGroupId);
161
+ if (!og || og.isAddOn)
162
+ continue;
163
+ const services = offering.services.filter((s) => s.optionGroupId === og.id);
164
+ if (services.length === 0)
165
+ continue;
166
+ services.forEach((s) => groupedServiceIds.add(s.id));
167
+ serviceGroups.push({
147
168
  id: generateId(),
148
- name: null,
149
- description: null,
150
- customValue: sl.customValue ?? null,
151
- metrics,
152
- };
153
- });
154
- }
155
- function mapStandaloneServices(soServices, tier, globalCurrency) {
156
- // Find services that are NOT in any service group
157
- const standaloneServices = soServices.filter((s) => !s.serviceGroupId);
158
- return standaloneServices
159
- .filter((svc) => {
160
- // Only include services that have a service level for this tier
161
- const level = tier.serviceLevels.find((sl) => sl.serviceId === svc.id);
162
- return (level &&
163
- level.level !== "NOT_INCLUDED" &&
164
- level.level !== "NOT_APPLICABLE");
165
- })
166
- .map((svc) => {
167
- const level = tier.serviceLevels.find((sl) => sl.serviceId === svc.id);
168
- const metrics = mapUsageLimits(svc.id, tier.usageLimits, globalCurrency);
169
- return {
169
+ name: og.name,
170
+ optional: false,
171
+ costType: og.costType ?? undefined,
172
+ recurringAmount: ogConfig.recurringAmount ?? undefined,
173
+ recurringCurrency: ogConfig.currency ?? globalCurrency,
174
+ recurringBillingCycle: ogConfig.effectiveBillingCycle,
175
+ recurringDiscount: mapResolvedDiscount(ogConfig.discount, og.discountMode === "INHERIT_TIER"
176
+ ? "TIER_INHERITED"
177
+ : "GROUP_INDEPENDENT"),
178
+ setupAmount: ogConfig.setupCost ?? undefined,
179
+ setupCurrency: ogConfig.setupCostCurrency ?? undefined,
180
+ services: services.map((svc) => mapServiceToInput(svc, tier, globalCurrency, ogConfig.effectiveBillingCycle)),
181
+ });
182
+ }
183
+ // Add-on option groups
184
+ for (const aoConfig of finalConfig.addOnConfigs) {
185
+ const og = offering.optionGroups.find((g) => g.id === aoConfig.optionGroupId);
186
+ if (!og)
187
+ continue;
188
+ const services = offering.services.filter((s) => s.optionGroupId === og.id);
189
+ if (services.length === 0)
190
+ continue;
191
+ services.forEach((s) => groupedServiceIds.add(s.id));
192
+ serviceGroups.push({
170
193
  id: generateId(),
171
- name: svc.title,
172
- description: svc.description ?? null,
173
- customValue: level?.customValue ?? null,
174
- metrics,
175
- };
176
- });
194
+ name: og.name,
195
+ optional: true,
196
+ costType: og.costType ?? undefined,
197
+ recurringAmount: aoConfig.recurringAmount ?? undefined,
198
+ recurringCurrency: aoConfig.currency ?? globalCurrency,
199
+ recurringBillingCycle: aoConfig.selectedBillingCycle,
200
+ recurringDiscount: mapResolvedDiscount(aoConfig.discount, og.discountMode === "INHERIT_TIER"
201
+ ? "TIER_INHERITED"
202
+ : "GROUP_INDEPENDENT"),
203
+ setupAmount: aoConfig.setupCost ?? undefined,
204
+ setupCurrency: aoConfig.setupCostCurrency ?? undefined,
205
+ services: services.map((svc) => mapServiceToInput(svc, tier, globalCurrency, aoConfig.selectedBillingCycle)),
206
+ });
207
+ }
208
+ }
209
+ /**
210
+ * Maps a single service from the offering to an InitializeServiceInput.
211
+ * Includes name, description, customValue from tier service levels,
212
+ * billing cycle, and usage metrics.
213
+ */
214
+ function mapServiceToInput(svc, tier, globalCurrency, billingCycle) {
215
+ const level = tier.serviceLevels.find((sl) => sl.serviceId === svc.id);
216
+ const metrics = mapUsageLimits(svc.id, tier.usageLimits, globalCurrency);
217
+ return {
218
+ id: generateId(),
219
+ name: svc.title,
220
+ description: svc.description ?? null,
221
+ customValue: level?.customValue ?? null,
222
+ recurringBillingCycle: billingCycle,
223
+ metrics,
224
+ };
177
225
  }
226
+ /**
227
+ * Maps a ResolvedDiscount from the offering to a DiscountInfoInitInput,
228
+ * or returns undefined if no discount.
229
+ */
230
+ function mapResolvedDiscount(discount, source) {
231
+ if (!discount)
232
+ return undefined;
233
+ return {
234
+ originalAmount: discount.originalAmount,
235
+ discountType: discount.discountType,
236
+ discountValue: discount.discountValue,
237
+ source,
238
+ };
239
+ }
240
+ /**
241
+ * Maps usage limits from the tier to InitializeMetricInput for a given service.
242
+ */
178
243
  function mapUsageLimits(serviceId, usageLimits, globalCurrency) {
179
244
  const limits = usageLimits.filter((ul) => ul.serviceId === serviceId);
180
245
  return limits.map((ul) => {
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/resources-services/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAsC5D,eAAO,MAAM,YAAY,GAAI,UAAU,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAuZxE,CAAC"}
1
+ {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/resources-services/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAsC5D,eAAO,MAAM,YAAY,GAAI,UAAU,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAoaxE,CAAC"}
@@ -277,6 +277,14 @@ export const getResolvers = (subgraph) => {
277
277
  resourceLabel: name,
278
278
  resourceThumbnailUrl: serviceOfferingState.thumbnailUrl,
279
279
  }));
280
+ // Set billing projection from tier price
281
+ const projectedAmount = subscriptionInput.tierPrice ?? finalConfiguration.tierBasePrice;
282
+ if (projectedAmount != null) {
283
+ await reactor.addAction(subscriptionInstanceDoc.header.id, SubscriptionInstance.actions.updateBillingProjection({
284
+ projectedBillAmount: projectedAmount,
285
+ projectedBillCurrency: finalConfiguration.tierCurrency || "USD",
286
+ }));
287
+ }
280
288
  return {
281
289
  success: true,
282
290
  data: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@powerhousedao/service-offering",
3
3
  "description": "service offering document models",
4
- "version": "0.0.6",
4
+ "version": "0.0.7",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "files": [