@powerhousedao/service-offering 1.0.0-dev.10 → 1.0.0-dev.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../../../document-models/subscription-instance/v1/src/reducers/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qCAAqC,EAAE,MAAM,0EAA0E,CAAC;AAetI,eAAO,MAAM,qCAAqC,EAAE,qCAsHjD,CAAC"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../../../../document-models/subscription-instance/v1/src/reducers/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qCAAqC,EAAE,MAAM,0EAA0E,CAAC;AAgBtI,eAAO,MAAM,qCAAqC,EAAE,qCAsHjD,CAAC"}
@@ -1,7 +1,8 @@
1
+ import { findService } from "./utils.js";
1
2
  import { AddServiceMetricServiceNotFoundError, UpdateMetricServiceNotFoundError, UpdateMetricNotFoundError, UpdateMetricUsageServiceNotFoundError, UpdateMetricUsageNotFoundError, RemoveServiceMetricServiceNotFoundError, RemoveServiceMetricNotFoundError, IncrementMetricUsageServiceNotFoundError, IncrementMetricUsageNotFoundError, DecrementMetricUsageServiceNotFoundError, DecrementMetricUsageNotFoundError, } from "../../gen/metrics/error.js";
2
3
  export const subscriptionInstanceMetricsOperations = {
3
4
  addServiceMetricOperation(state, action) {
4
- const svc = state.services.find((s) => s.id === action.input.serviceId);
5
+ const svc = findService(state, action.input.serviceId);
5
6
  if (!svc) {
6
7
  throw new AddServiceMetricServiceNotFoundError(`Service with ID ${action.input.serviceId} not found`);
7
8
  }
@@ -30,7 +31,7 @@ export const subscriptionInstanceMetricsOperations = {
30
31
  });
31
32
  },
32
33
  updateMetricOperation(state, action) {
33
- const svc = state.services.find((s) => s.id === action.input.serviceId);
34
+ const svc = findService(state, action.input.serviceId);
34
35
  if (!svc) {
35
36
  throw new UpdateMetricServiceNotFoundError(`Service with ID ${action.input.serviceId} not found`);
36
37
  }
@@ -50,7 +51,7 @@ export const subscriptionInstanceMetricsOperations = {
50
51
  metric.nextUsageReset = action.input.nextUsageReset || null;
51
52
  },
52
53
  updateMetricUsageOperation(state, action) {
53
- const svc = state.services.find((s) => s.id === action.input.serviceId);
54
+ const svc = findService(state, action.input.serviceId);
54
55
  if (!svc) {
55
56
  throw new UpdateMetricUsageServiceNotFoundError(`Service with ID ${action.input.serviceId} not found`);
56
57
  }
@@ -61,7 +62,7 @@ export const subscriptionInstanceMetricsOperations = {
61
62
  metric.currentUsage = action.input.currentUsage;
62
63
  },
63
64
  removeServiceMetricOperation(state, action) {
64
- const svc = state.services.find((s) => s.id === action.input.serviceId);
65
+ const svc = findService(state, action.input.serviceId);
65
66
  if (!svc) {
66
67
  throw new RemoveServiceMetricServiceNotFoundError(`Service with ID ${action.input.serviceId} not found`);
67
68
  }
@@ -72,7 +73,7 @@ export const subscriptionInstanceMetricsOperations = {
72
73
  svc.metrics.splice(index, 1);
73
74
  },
74
75
  incrementMetricUsageOperation(state, action) {
75
- const svc = state.services.find((s) => s.id === action.input.serviceId);
76
+ const svc = findService(state, action.input.serviceId);
76
77
  if (!svc) {
77
78
  throw new IncrementMetricUsageServiceNotFoundError(`Service with ID ${action.input.serviceId} not found`);
78
79
  }
@@ -83,7 +84,7 @@ export const subscriptionInstanceMetricsOperations = {
83
84
  metric.currentUsage += action.input.incrementBy;
84
85
  },
85
86
  decrementMetricUsageOperation(state, action) {
86
- const svc = state.services.find((s) => s.id === action.input.serviceId);
87
+ const svc = findService(state, action.input.serviceId);
87
88
  if (!svc) {
88
89
  throw new DecrementMetricUsageServiceNotFoundError(`Service with ID ${action.input.serviceId} not found`);
89
90
  }
@@ -0,0 +1,7 @@
1
+ import type { SubscriptionInstanceState, Service } from "../../gen/schema/types.js";
2
+ /**
3
+ * Finds a service by ID across both top-level standalone services
4
+ * and services nested inside service groups.
5
+ */
6
+ export declare function findService(state: SubscriptionInstanceState, serviceId: string): Service | undefined;
7
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../../document-models/subscription-instance/v1/src/reducers/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,OAAO,EACR,MAAM,2BAA2B,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,yBAAyB,EAChC,SAAS,EAAE,MAAM,GAChB,OAAO,GAAG,SAAS,CAQrB"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Finds a service by ID across both top-level standalone services
3
+ * and services nested inside service groups.
4
+ */
5
+ export function findService(state, serviceId) {
6
+ const standalone = state.services.find((s) => s.id === serviceId);
7
+ if (standalone)
8
+ return standalone;
9
+ for (const group of state.serviceGroups) {
10
+ const grouped = group.services.find((s) => s.id === serviceId);
11
+ if (grouped)
12
+ return grouped;
13
+ }
14
+ return undefined;
15
+ }
@@ -1,6 +1,45 @@
1
1
  import { generateMock } from "@powerhousedao/common/utils";
2
+ import { generateId } from "document-model/core";
2
3
  import { describe, expect, it } from "vitest";
3
- import { reducer, utils, isSubscriptionInstanceDocument, addServiceMetric, updateMetric, updateMetricUsage, removeServiceMetric, incrementMetricUsage, decrementMetricUsage, AddServiceMetricInputSchema, UpdateMetricInputSchema, UpdateMetricUsageInputSchema, RemoveServiceMetricInputSchema, IncrementMetricUsageInputSchema, DecrementMetricUsageInputSchema, } from "@powerhousedao/service-offering/document-models/subscription-instance/v1";
4
+ import { reducer, utils, isSubscriptionInstanceDocument, initializeSubscription, addServiceMetric, updateMetric, updateMetricUsage, removeServiceMetric, incrementMetricUsage, decrementMetricUsage, AddServiceMetricInputSchema, UpdateMetricInputSchema, UpdateMetricUsageInputSchema, RemoveServiceMetricInputSchema, IncrementMetricUsageInputSchema, DecrementMetricUsageInputSchema, } from "@powerhousedao/service-offering/document-models/subscription-instance/v1";
5
+ /** Creates a document with a service group containing a service with a metric. */
6
+ function createDocWithServiceGroup() {
7
+ const serviceId = generateId();
8
+ const metricId = generateId();
9
+ const groupId = generateId();
10
+ let doc = utils.createDocument();
11
+ doc = reducer(doc, initializeSubscription({
12
+ createdAt: new Date().toISOString(),
13
+ customerName: "Test",
14
+ selectedBillingCycle: "MONTHLY",
15
+ globalCurrency: "USD",
16
+ autoRenew: true,
17
+ serviceGroups: [
18
+ {
19
+ id: groupId,
20
+ name: "Test Group",
21
+ optional: false,
22
+ services: [
23
+ {
24
+ id: serviceId,
25
+ name: "Test Service",
26
+ metrics: [
27
+ {
28
+ id: metricId,
29
+ name: "API Calls",
30
+ unitName: "calls",
31
+ currentUsage: 100,
32
+ freeLimit: 50,
33
+ paidLimit: 200,
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ }));
41
+ return { doc, serviceId, metricId, groupId };
42
+ }
4
43
  describe("MetricsOperations", () => {
5
44
  it("should handle addServiceMetric operation", () => {
6
45
  const document = utils.createDocument();
@@ -62,4 +101,52 @@ describe("MetricsOperations", () => {
62
101
  expect(updatedDocument.operations.global[0].action.input).toStrictEqual(input);
63
102
  expect(updatedDocument.operations.global[0].index).toEqual(0);
64
103
  });
104
+ describe("service group metric operations", () => {
105
+ it("should increment metric usage for a service inside a service group", () => {
106
+ const { doc, serviceId, metricId } = createDocWithServiceGroup();
107
+ const updatedDoc = reducer(doc, incrementMetricUsage({
108
+ serviceId,
109
+ metricId,
110
+ incrementBy: 10,
111
+ currentTime: new Date().toISOString(),
112
+ }));
113
+ const metric = updatedDoc.state.global.serviceGroups[0].services[0].metrics[0];
114
+ expect(metric.currentUsage).toBe(110);
115
+ expect(updatedDoc.operations.global[1].error).toBeUndefined();
116
+ });
117
+ it("should decrement metric usage for a service inside a service group", () => {
118
+ const { doc, serviceId, metricId } = createDocWithServiceGroup();
119
+ const updatedDoc = reducer(doc, decrementMetricUsage({
120
+ serviceId,
121
+ metricId,
122
+ decrementBy: 25,
123
+ currentTime: new Date().toISOString(),
124
+ }));
125
+ const metric = updatedDoc.state.global.serviceGroups[0].services[0].metrics[0];
126
+ expect(metric.currentUsage).toBe(75);
127
+ expect(updatedDoc.operations.global[1].error).toBeUndefined();
128
+ });
129
+ it("should set metric usage for a service inside a service group", () => {
130
+ const { doc, serviceId, metricId } = createDocWithServiceGroup();
131
+ const updatedDoc = reducer(doc, updateMetricUsage({
132
+ serviceId,
133
+ metricId,
134
+ currentUsage: 999,
135
+ currentTime: new Date().toISOString(),
136
+ }));
137
+ const metric = updatedDoc.state.global.serviceGroups[0].services[0].metrics[0];
138
+ expect(metric.currentUsage).toBe(999);
139
+ expect(updatedDoc.operations.global[1].error).toBeUndefined();
140
+ });
141
+ it("should return error for non-existent service in service group", () => {
142
+ const { doc, metricId } = createDocWithServiceGroup();
143
+ const updatedDoc = reducer(doc, incrementMetricUsage({
144
+ serviceId: "non-existent-id",
145
+ metricId,
146
+ incrementBy: 1,
147
+ currentTime: new Date().toISOString(),
148
+ }));
149
+ expect(updatedDoc.operations.global[1].error).toBe("Service with ID non-existent-id not found");
150
+ });
151
+ });
65
152
  });
@@ -1 +1 @@
1
- {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/resources-services/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAyD/D,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAsfxB,CAAC"}
1
+ {"version":3,"file":"resolvers.d.ts","sourceRoot":"","sources":["../../../subgraphs/resources-services/resolvers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAyD/D,eAAO,MAAM,YAAY,GACvB,UAAU,YAAY,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CA6gBxB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { getUserSelectionPriceBreakdown, } from "../../document-models/service-offering/v1/src/utils.js";
2
- import { createAction } from "document-model/core";
3
- import { addFile, driveCreateDocument } from "document-drive";
2
+ import { createAction, generateId } from "document-model/core";
3
+ import { addFile, addFolder, driveCreateDocument } from "document-drive";
4
4
  import { ResourceInstance } from "../../document-models/resource-instance/v1/module.js";
5
5
  import { SubscriptionInstance } from "../../document-models/subscription-instance/v1/module.js";
6
6
  import { mapOfferingToSubscription } from "../../editors/subscription-instance-editor/components/mapOfferingToSubscription.js";
@@ -269,26 +269,29 @@ export const getResolvers = (subgraph) => {
269
269
  const builderProfileDoc = await reactorClient.createEmpty("powerhouse/builder-profile", { parentIdentifier: driveId });
270
270
  const resourceInstanceDoc = await reactorClient.createEmpty("powerhouse/resource-instance", { parentIdentifier: driveId });
271
271
  const subscriptionInstanceDoc = await reactorClient.createEmpty("powerhouse/subscription-instance", { parentIdentifier: driveId });
272
- // add file references to the team drive
273
- const teamRootFolder = teamBuilderAdminDrive.state.global.nodes.find((node) => node.kind === "folder")?.parentFolder;
272
+ // create "Service Subscriptions" folder and organize files in team drive
273
+ const teamServiceSubsFolderId = generateId();
274
274
  await reactorClient.execute(driveId, "main", [
275
+ addFolder({
276
+ id: teamServiceSubsFolderId,
277
+ name: "Service Subscriptions",
278
+ }),
275
279
  addFile({
276
280
  documentType: "powerhouse/builder-profile",
277
281
  id: builderProfileDoc.header.id,
278
282
  name: `${parsedTeamName} Builder Profile`,
279
- parentFolder: teamRootFolder,
280
283
  }),
281
284
  addFile({
282
285
  documentType: "powerhouse/resource-instance",
283
286
  id: resourceInstanceDoc.header.id,
284
287
  name: `${parsedTeamName} Resource Instance`,
285
- parentFolder: teamRootFolder,
288
+ parentFolder: teamServiceSubsFolderId,
286
289
  }),
287
290
  addFile({
288
291
  documentType: "powerhouse/subscription-instance",
289
292
  id: subscriptionInstanceDoc.header.id,
290
293
  name: `${parsedTeamName} Subscription Instance`,
291
- parentFolder: teamRootFolder,
294
+ parentFolder: teamServiceSubsFolderId,
292
295
  }),
293
296
  ]);
294
297
  // update builder profile
@@ -307,26 +310,44 @@ export const getResolvers = (subgraph) => {
307
310
  if (!operatorProfileId) {
308
311
  throw new Error(`Operator profile not found for drive ${operatorDrive.header.id}`);
309
312
  }
310
- const operatorParentFolder = operatorDrive.state.global.nodes.find((node) => node.kind === "folder")?.parentFolder;
313
+ // find or create "Service Subscriptions" folder in the operator drive
314
+ let serviceSubscriptionsFolderId = operatorDrive.state.global.nodes.find((node) => node.kind === "folder" &&
315
+ node.name === "Service Subscriptions")?.id;
316
+ if (!serviceSubscriptionsFolderId) {
317
+ serviceSubscriptionsFolderId = generateId();
318
+ await reactorClient.execute(operatorDrive.header.id, "main", [
319
+ addFolder({
320
+ id: serviceSubscriptionsFolderId,
321
+ name: "Service Subscriptions",
322
+ }),
323
+ ]);
324
+ }
325
+ // create a team folder inside "Service Subscriptions" for this team's docs
326
+ const teamFolderId = generateId();
311
327
  // add reactor-level relationships so Connect syncs the child documents
312
328
  // (createEmpty guarantees CREATE_DOCUMENT is persisted before this runs)
313
329
  await reactorClient.addChildren(operatorDrive.header.id, [
314
330
  resourceInstanceDoc.header.id,
315
331
  subscriptionInstanceDoc.header.id,
316
332
  ]);
317
- // add file references to operator drive
333
+ // add team folder and file references to operator drive
318
334
  await reactorClient.execute(operatorDrive.header.id, "main", [
335
+ addFolder({
336
+ id: teamFolderId,
337
+ name: teamName,
338
+ parentFolder: serviceSubscriptionsFolderId,
339
+ }),
319
340
  addFile({
320
341
  documentType: "powerhouse/resource-instance",
321
342
  id: resourceInstanceDoc.header.id,
322
343
  name: `${parsedTeamName} Resource Instance`,
323
- parentFolder: operatorParentFolder,
344
+ parentFolder: teamFolderId,
324
345
  }),
325
346
  addFile({
326
347
  documentType: "powerhouse/subscription-instance",
327
348
  id: subscriptionInstanceDoc.header.id,
328
349
  name: `${parsedTeamName} Subscription Instance`,
329
- parentFolder: operatorParentFolder,
350
+ parentFolder: teamFolderId,
330
351
  }),
331
352
  ]);
332
353
  // populate documents after all files are added to both drives
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": "1.0.0-dev.10",
4
+ "version": "1.0.0-dev.12",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",