Skip to content

Commit 1ef1864

Browse files
committed
tests 2.14 changes
1 parent 981ac8a commit 1ef1864

1 file changed

Lines changed: 287 additions & 78 deletions

File tree

apps/backend-services/src/billing/usage-query.service.spec.ts

Lines changed: 287 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { NotFoundException } from "@nestjs/common";
1+
import { ForbiddenException, NotFoundException } from "@nestjs/common";
22
import { Test, TestingModule } from "@nestjs/testing";
33
import { PrismaService } from "@/database/prisma.service";
4+
import { UsageEventType } from "@/generated";
45
import { UsageQueryService } from "./usage-query.service";
56

67
const mockPrisma = {
@@ -14,6 +15,29 @@ const mockPrisma = {
1415
},
1516
usageEvent: {
1617
findMany: jest.fn(),
18+
findFirst: jest.fn().mockResolvedValue({
19+
rate_version: {
20+
id: "123",
21+
created_at: new Date(),
22+
version: "1.0.0",
23+
effective_from: new Date(),
24+
unit_cost_dollars: 0.001,
25+
units_per_gb_per_month: 0.1,
26+
max_pages_assumption: 10,
27+
max_array_items_assumption: 3,
28+
},
29+
id: "abc",
30+
group_id: "group-1",
31+
created_at: new Date(),
32+
event_type: UsageEventType.activity_completed,
33+
workflow_execution_id: "abc-123",
34+
workflow_version_id: "1.0.0",
35+
activity_name: "azureOcr.submit",
36+
metered_quantity: 3,
37+
units_consumed: 2,
38+
unit_cost_dollars: 3,
39+
rate_version_id: "132112",
40+
}),
1741
},
1842
group: {
1943
findMany: jest.fn(),
@@ -30,7 +54,7 @@ const mockPrisma = {
3054
const mockPrismaService = { prisma: mockPrisma };
3155

3256
function makeDecimal(n: number) {
33-
return { toNumber: () => n };
57+
return { toNumber: () => n, equals: (v: number) => n === v };
3458
}
3559

3660
describe("UsageQueryService", () => {
@@ -146,82 +170,267 @@ describe("UsageQueryService", () => {
146170
// ---------------------------------------------------------------------------
147171
// getRunDetail
148172
// ---------------------------------------------------------------------------
149-
// describe("getRunDetail", () => {
150-
// it("throws NotFoundException when no events exist for the execution", async () => {
151-
// mockPrisma.usageEvent.findMany.mockResolvedValue([]);
152-
// await expect(service.getRunDetail("group-1", "exec-abc")).rejects.toThrow(
153-
// NotFoundException,
154-
// );
155-
// });
156-
157-
// it("throws ForbiddenException when execution belongs to a different group", async () => {
158-
// mockPrisma.usageEvent.findMany.mockResolvedValue([
159-
// {
160-
// id: "evt-1",
161-
// group_id: "group-other",
162-
// event_type: "workflow_started",
163-
// activity_name: null,
164-
// units_consumed: makeDecimal(0),
165-
// estimated_units: null,
166-
// metered_quantity: null,
167-
// created_at: new Date(),
168-
// rate_version: { unit_cost_dollars: makeDecimal(0.001) },
169-
// },
170-
// ]);
171-
172-
// await expect(service.getRunDetail("group-1", "exec-abc")).rejects.toThrow(
173-
// ForbiddenException,
174-
// );
175-
// });
176-
177-
// it("returns correct run detail with dollar values computed", async () => {
178-
// const createdAt = new Date("2026-06-01T10:00:00Z");
179-
// mockPrisma.usageEvent.findMany.mockResolvedValue([
180-
// {
181-
// id: "evt-1",
182-
// group_id: "group-1",
183-
// event_type: "workflow_started",
184-
// activity_name: null,
185-
// units_consumed: makeDecimal(0),
186-
// estimated_units: makeDecimal(50),
187-
// metered_quantity: null,
188-
// created_at: createdAt,
189-
// rate_version: { unit_cost_dollars: makeDecimal(0.001) },
190-
// },
191-
// {
192-
// id: "evt-2",
193-
// group_id: "group-1",
194-
// event_type: "activity_completed",
195-
// activity_name: "azureOcr.extract",
196-
// units_consumed: makeDecimal(40),
197-
// estimated_units: null,
198-
// metered_quantity: 4,
199-
// created_at: createdAt,
200-
// rate_version: { unit_cost_dollars: makeDecimal(0.001) },
201-
// },
202-
// {
203-
// id: "evt-3",
204-
// group_id: "group-1",
205-
// event_type: "workflow_completed",
206-
// activity_name: null,
207-
// units_consumed: makeDecimal(40),
208-
// estimated_units: null,
209-
// metered_quantity: null,
210-
// created_at: createdAt,
211-
// rate_version: { unit_cost_dollars: makeDecimal(0.001) },
212-
// },
213-
// ]);
214-
215-
// const result = await service.getRunDetail("group-1", "exec-abc");
216-
217-
// expect(result.workflow_execution_id).toBe("exec-abc");
218-
// expect(result.estimated_units).toBe(50);
219-
// expect(result.total_units_consumed).toBe(40);
220-
// expect(result.events).toHaveLength(3);
221-
// expect(result.events[1].dollar_value).toBe(0.04); // 40 * 0.001
222-
// expect(result.events[1].metered_quantity).toBe(4);
223-
// });
224-
// });
173+
describe("getRunDetail", () => {
174+
it("throws NotFoundException when no events exist for the execution", async () => {
175+
mockPrisma.usageEvent.findFirst.mockResolvedValue(null);
176+
await expect(service.getRunDetail("group-1", "exec-abc")).rejects.toThrow(
177+
NotFoundException,
178+
);
179+
});
180+
181+
it("throws ForbiddenException when execution belongs to a different group", async () => {
182+
const createdAt = new Date("2026-06-01T10:00:00Z");
183+
mockPrisma.usageEvent.findFirst.mockResolvedValue({
184+
id: "evt-1",
185+
group_id: "group-other",
186+
event_type: "workflow_cost",
187+
activity_name: null,
188+
units_consumed: makeDecimal(10),
189+
estimated_units: null,
190+
metered_quantity: null,
191+
created_at: createdAt,
192+
workflow_version_id: "v1",
193+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
194+
});
195+
196+
await expect(service.getRunDetail("group-1", "exec-abc")).rejects.toThrow(
197+
ForbiddenException,
198+
);
199+
});
200+
201+
it("returns correct run detail with dollar values computed", async () => {
202+
const createdAt = new Date("2026-06-01T10:00:00Z");
203+
mockPrisma.usageEvent.findFirst.mockResolvedValue({
204+
id: "evt-2",
205+
group_id: "group-1",
206+
event_type: "workflow_cost",
207+
activity_name: null,
208+
units_consumed: makeDecimal(40),
209+
estimated_units: makeDecimal(50),
210+
metered_quantity: 4,
211+
created_at: createdAt,
212+
workflow_version_id: "v-1",
213+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
214+
});
215+
216+
const result = await service.getRunDetail("group-1", "exec-abc");
217+
218+
expect(result.workflow_execution_id).toBe("exec-abc");
219+
expect(result.group_id).toBe("group-1");
220+
expect(result.estimated_units).toBe(50);
221+
expect(result.total_units_consumed).toBe(40);
222+
expect(result.units_consumed).toBe(40);
223+
expect(result.dollar_value).toBeCloseTo(0.04);
224+
expect(result.workflow_version_id).toBe("v-1");
225+
});
226+
227+
it("returns null estimated_units when event has no estimated_units", async () => {
228+
const createdAt = new Date("2026-06-01T10:00:00Z");
229+
mockPrisma.usageEvent.findFirst.mockResolvedValue({
230+
id: "evt-3",
231+
group_id: "group-1",
232+
event_type: "workflow_cost",
233+
activity_name: null,
234+
units_consumed: makeDecimal(5),
235+
estimated_units: null,
236+
metered_quantity: null,
237+
created_at: createdAt,
238+
workflow_version_id: "v-2",
239+
rate_version: { unit_cost_dollars: makeDecimal(0.01) },
240+
});
241+
242+
const result = await service.getRunDetail("group-1", "exec-abc");
243+
expect(result.estimated_units).toBeNull();
244+
expect(result.workflow_version_id).toBe("v-2");
245+
});
246+
});
247+
248+
// ---------------------------------------------------------------------------
249+
// getGroupActivityHistory
250+
// ---------------------------------------------------------------------------
251+
describe("getGroupActivityHistory", () => {
252+
it("returns empty array when no events exist", async () => {
253+
mockPrisma.usageEvent.findMany.mockResolvedValue([]);
254+
const result = await service.getGroupActivityHistory("group-1");
255+
expect(result).toEqual([]);
256+
});
257+
258+
it("skips events with zero units_consumed", async () => {
259+
mockPrisma.usageEvent.findMany.mockResolvedValue([
260+
{
261+
activity_name: "azureOcr.extract",
262+
units_consumed: makeDecimal(0),
263+
created_at: new Date("2026-06-15T00:00:00Z"),
264+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
265+
event_type: "activity_completed",
266+
workflow_version_id: "v1",
267+
},
268+
]);
269+
270+
const result = await service.getGroupActivityHistory("group-1");
271+
expect(result).toEqual([]);
272+
});
273+
274+
it("groups a single activity_completed event by activity_name", async () => {
275+
mockPrisma.usageEvent.findMany.mockResolvedValue([
276+
{
277+
activity_name: "azureOcr.extract",
278+
units_consumed: makeDecimal(10),
279+
created_at: new Date("2026-06-15T00:00:00Z"),
280+
rate_version: { unit_cost_dollars: makeDecimal(0.002) },
281+
event_type: "activity_completed",
282+
workflow_version_id: "v1",
283+
},
284+
]);
285+
286+
const result = await service.getGroupActivityHistory("group-1");
287+
expect(result).toHaveLength(1);
288+
expect(result[0].period_year).toBe(2026);
289+
expect(result[0].period_month).toBe(6);
290+
expect(result[0].event_type).toBe("activity_completed");
291+
expect(result[0].units_consumed).toBe(10);
292+
expect(result[0].dollars_spent).toBeCloseTo(0.02);
293+
expect(result[0].activities["azureOcr.extract"]).toEqual({
294+
units_consumed: 10,
295+
dollars_spent: expect.closeTo(0.02),
296+
});
297+
});
298+
299+
it("uses workflow_version_id as activity key for workflow_cost events", async () => {
300+
mockPrisma.usageEvent.findMany.mockResolvedValue([
301+
{
302+
activity_name: null,
303+
units_consumed: makeDecimal(20),
304+
created_at: new Date("2026-06-15T00:00:00Z"),
305+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
306+
event_type: "workflow_cost",
307+
workflow_version_id: "workflow-v2",
308+
},
309+
]);
310+
311+
const result = await service.getGroupActivityHistory("group-1");
312+
expect(result).toHaveLength(1);
313+
expect(result[0].event_type).toBe("workflow_cost");
314+
expect(result[0].activities["workflow-v2"]).toBeDefined();
315+
expect(result[0].activities["workflow-v2"].units_consumed).toBe(20);
316+
});
317+
318+
it("falls back to 'other' when activity_name is null for non-workflow_cost events", async () => {
319+
mockPrisma.usageEvent.findMany.mockResolvedValue([
320+
{
321+
activity_name: null,
322+
units_consumed: makeDecimal(5),
323+
created_at: new Date("2026-06-15T00:00:00Z"),
324+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
325+
event_type: "activity_completed",
326+
workflow_version_id: null,
327+
},
328+
]);
329+
330+
const result = await service.getGroupActivityHistory("group-1");
331+
expect(result[0].activities["other"]).toBeDefined();
332+
expect(result[0].activities["other"].units_consumed).toBe(5);
333+
});
334+
335+
it("aggregates multiple events in the same bucket", async () => {
336+
const date = new Date("2026-06-15T00:00:00Z");
337+
mockPrisma.usageEvent.findMany.mockResolvedValue([
338+
{
339+
activity_name: "azureOcr.extract",
340+
units_consumed: makeDecimal(10),
341+
created_at: date,
342+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
343+
event_type: "activity_completed",
344+
workflow_version_id: "v1",
345+
},
346+
{
347+
activity_name: "azureOcr.extract",
348+
units_consumed: makeDecimal(5),
349+
created_at: date,
350+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
351+
event_type: "activity_completed",
352+
workflow_version_id: "v1",
353+
},
354+
]);
355+
356+
const result = await service.getGroupActivityHistory("group-1");
357+
expect(result).toHaveLength(1);
358+
expect(result[0].units_consumed).toBe(15);
359+
expect(result[0].activities["azureOcr.extract"].units_consumed).toBe(15);
360+
});
361+
362+
it("produces separate buckets for different activity types in same month", async () => {
363+
const date = new Date("2026-06-15T00:00:00Z");
364+
mockPrisma.usageEvent.findMany.mockResolvedValue([
365+
{
366+
activity_name: "azureOcr.extract",
367+
units_consumed: makeDecimal(10),
368+
created_at: date,
369+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
370+
event_type: "activity_completed",
371+
workflow_version_id: "v1",
372+
},
373+
{
374+
activity_name: "formRecognizer.analyze",
375+
units_consumed: makeDecimal(8),
376+
created_at: date,
377+
rate_version: { unit_cost_dollars: makeDecimal(0.002) },
378+
event_type: "activity_completed",
379+
workflow_version_id: "v1",
380+
},
381+
]);
382+
383+
const result = await service.getGroupActivityHistory("group-1");
384+
// Same event_type + workflow_version_id → same bucket
385+
expect(result).toHaveLength(1);
386+
expect(result[0].units_consumed).toBe(18);
387+
expect(result[0].activities["azureOcr.extract"]).toBeDefined();
388+
expect(result[0].activities["formRecognizer.analyze"]).toBeDefined();
389+
});
390+
391+
it("produces separate results for different months, sorted ascending", async () => {
392+
mockPrisma.usageEvent.findMany.mockResolvedValue([
393+
{
394+
activity_name: "azureOcr.extract",
395+
units_consumed: makeDecimal(10),
396+
created_at: new Date("2026-07-10T00:00:00Z"),
397+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
398+
event_type: "activity_completed",
399+
workflow_version_id: "v1",
400+
},
401+
{
402+
activity_name: "azureOcr.extract",
403+
units_consumed: makeDecimal(5),
404+
created_at: new Date("2026-06-10T00:00:00Z"),
405+
rate_version: { unit_cost_dollars: makeDecimal(0.001) },
406+
event_type: "activity_completed",
407+
workflow_version_id: "v1",
408+
},
409+
]);
410+
411+
const result = await service.getGroupActivityHistory("group-1");
412+
expect(result).toHaveLength(2);
413+
expect(result[0].period_month).toBe(6);
414+
expect(result[1].period_month).toBe(7);
415+
});
416+
417+
it("passes date filters through to the Prisma query", async () => {
418+
mockPrisma.usageEvent.findMany.mockResolvedValue([]);
419+
const start = new Date("2026-01-01");
420+
const end = new Date("2026-06-30");
421+
422+
await service.getGroupActivityHistory("group-1", start, end);
423+
424+
expect(mockPrisma.usageEvent.findMany).toHaveBeenCalledWith(
425+
expect.objectContaining({
426+
where: expect.objectContaining({
427+
group_id: "group-1",
428+
created_at: { gte: start, lte: end },
429+
}),
430+
}),
431+
);
432+
});
433+
});
225434

226435
// ---------------------------------------------------------------------------
227436
// getAllGroupsSummary

0 commit comments

Comments
 (0)