Skip to content

Commit 1244dc7

Browse files
authored
Invoicing timezones (#5515)
1 parent 6472ccc commit 1244dc7

File tree

17 files changed

+328
-179
lines changed

17 files changed

+328
-179
lines changed

bifrost/lib/clients/jawnTypes/private.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16595,6 +16595,8 @@ Json: JsonObject;
1659516595
endDate: string;
1659616596
/** Format: double */
1659716597
amountCents: number;
16598+
/** Format: double */
16599+
subtotalCents: number | null;
1659816600
notes: string | null;
1659916601
createdAt: string;
1660016602
};
@@ -16772,6 +16774,8 @@ Json: JsonObject;
1677216774
dashboardUrl: string;
1677316775
/** Format: double */
1677416776
amountCents: number;
16777+
/** Format: double */
16778+
subtotalCents: number;
1677516779
ptbInvoiceId: string;
1677616780
};
1677716781
ResultSuccess_CreateInvoiceResponse_: {

bifrost/lib/clients/jawnTypes/public.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4734,6 +4734,8 @@ Json: JsonObject;
47344734
endDate: string;
47354735
/** Format: double */
47364736
amountCents: number;
4737+
/** Format: double */
4738+
subtotalCents: number | null;
47374739
notes: string | null;
47384740
createdAt: string;
47394741
};

docs/swagger.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14385,6 +14385,11 @@
1438514385
"type": "number",
1438614386
"format": "double"
1438714387
},
14388+
"subtotalCents": {
14389+
"type": "number",
14390+
"format": "double",
14391+
"nullable": true
14392+
},
1438814393
"notes": {
1438914394
"type": "string",
1439014395
"nullable": true
@@ -14401,6 +14406,7 @@
1440114406
"startDate",
1440214407
"endDate",
1440314408
"amountCents",
14409+
"subtotalCents",
1440414410
"notes",
1440514411
"createdAt"
1440614412
],

helicone-mcp/src/types/public.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4734,6 +4734,8 @@ Json: JsonObject;
47344734
endDate: string;
47354735
/** Format: double */
47364736
amountCents: number;
4737+
/** Format: double */
4738+
subtotalCents: number | null;
47374739
notes: string | null;
47384740
createdAt: string;
47394741
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Add subtotal_cents to track pre-discount amount for wallet crediting
2+
-- Nullable for backwards compatibility with existing invoices
3+
ALTER TABLE "public"."ptb_invoices"
4+
ADD COLUMN "subtotal_cents" BIGINT;

valhalla/jawn/src/controllers/private/adminWalletController.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ import {
3030
CreateInvoiceResponse,
3131
} from "../../managers/admin/InvoicingManager";
3232

33+
// Date normalization helpers for invoice date boundaries
34+
const normalizeToStartOfDay = (dateStr: string): Date => {
35+
const date = new Date(dateStr);
36+
date.setUTCHours(0, 0, 0, 0);
37+
return date;
38+
};
39+
40+
const normalizeToEndOfDay = (dateStr: string): Date => {
41+
const date = new Date(dateStr);
42+
date.setUTCHours(23, 59, 59, 999);
43+
return date;
44+
};
45+
3346
interface DashboardData {
3447
organizations: Array<{
3548
orgId: string;
@@ -566,8 +579,8 @@ export class AdminWalletController extends Controller {
566579
): Promise<Result<ModelSpend[], string>> {
567580
await authCheckThrow(request.authParams.userId);
568581

569-
const start = new Date(startDate);
570-
const end = new Date(endDate);
582+
const start = normalizeToStartOfDay(startDate);
583+
const end = normalizeToEndOfDay(endDate);
571584

572585
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
573586
return err("Invalid date format");
@@ -668,8 +681,8 @@ export class AdminWalletController extends Controller {
668681
): Promise<Result<CreateInvoiceResponse, string>> {
669682
await authCheckThrow(request.authParams.userId);
670683

671-
const start = new Date(body.startDate);
672-
const end = new Date(body.endDate);
684+
const start = normalizeToStartOfDay(body.startDate);
685+
const end = normalizeToEndOfDay(body.endDate);
673686

674687
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
675688
return err("Invalid date format");

valhalla/jawn/src/managers/admin/InvoicingManager.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface CreateInvoiceResponse {
2626
hostedInvoiceUrl: string | null;
2727
dashboardUrl: string;
2828
amountCents: number;
29+
subtotalCents: number;
2930
ptbInvoiceId: string;
3031
}
3132

@@ -173,16 +174,8 @@ export class InvoicingManager {
173174
});
174175

175176
// 5. Create the invoice (in draft mode)
176-
const periodStart = startDate.toLocaleDateString("en-US", {
177-
month: "short",
178-
day: "numeric",
179-
year: "numeric",
180-
});
181-
const periodEnd = endDate.toLocaleDateString("en-US", {
182-
month: "short",
183-
day: "numeric",
184-
year: "numeric",
185-
});
177+
const periodStart = startDate.toISOString().split("T")[0];
178+
const periodEnd = endDate.toISOString().split("T")[0];
186179

187180
const invoice = await stripe.invoices.create({
188181
customer: stripeCustomerId,
@@ -200,6 +193,7 @@ export class InvoicingManager {
200193

201194
// 6. Create invoice items with discounts applied
202195
let totalAmountCents = 0;
196+
let totalSubtotalCents = 0;
203197

204198
for (const item of spendResult.data) {
205199
const baseSubtotalUsd = parseFloat(item.cost);
@@ -216,11 +210,13 @@ export class InvoicingManager {
216210

217211
const discountPercent = findDiscount(discounts, item.model, item.provider);
218212
const totalUsd = subtotalUsd * (1 - discountPercent / 100);
213+
const subtotalCents = Math.round(subtotalUsd * 100);
219214
const amountCents = Math.round(totalUsd * 100);
220215

221216
if (amountCents <= 0) continue;
222217

223218
totalAmountCents += amountCents;
219+
totalSubtotalCents += subtotalCents;
224220

225221
const promptTokens = parseInt(item.prompt_tokens, 10) || 0;
226222
const completionTokens = parseInt(item.completion_tokens, 10) || 0;
@@ -240,21 +236,39 @@ export class InvoicingManager {
240236
});
241237
}
242238

243-
// 7. Record in ptb_invoices
244-
const ptbResult = await dbExecute<{ id: string }>(
245-
`INSERT INTO ptb_invoices (organization_id, stripe_invoice_id, start_date, end_date, amount_cents, notes)
246-
VALUES ($1, $2, $3, $4, $5, $6)
239+
// 7. Record in ptb_invoices (with fallback if subtotal_cents column doesn't exist yet)
240+
let ptbResult = await dbExecute<{ id: string }>(
241+
`INSERT INTO ptb_invoices (organization_id, stripe_invoice_id, start_date, end_date, amount_cents, subtotal_cents, notes)
242+
VALUES ($1, $2, $3, $4, $5, $6, $7)
247243
RETURNING id`,
248244
[
249245
this.orgId,
250246
invoice.id,
251247
startDate,
252248
endDate,
253249
totalAmountCents,
250+
totalSubtotalCents,
254251
`Auto-created invoice for ${orgName}`,
255252
]
256253
);
257254

255+
// If subtotal_cents column doesn't exist yet, fall back to insert without it
256+
if (ptbResult.error && JSON.stringify(ptbResult.error).includes("42703")) {
257+
ptbResult = await dbExecute<{ id: string }>(
258+
`INSERT INTO ptb_invoices (organization_id, stripe_invoice_id, start_date, end_date, amount_cents, notes)
259+
VALUES ($1, $2, $3, $4, $5, $6)
260+
RETURNING id`,
261+
[
262+
this.orgId,
263+
invoice.id,
264+
startDate,
265+
endDate,
266+
totalAmountCents,
267+
`Auto-created invoice for ${orgName}`,
268+
]
269+
);
270+
}
271+
258272
if (ptbResult.error) {
259273
console.error("Failed to record invoice in ptb_invoices:", ptbResult.error);
260274
}
@@ -266,6 +280,7 @@ export class InvoicingManager {
266280
hostedInvoiceUrl: null,
267281
dashboardUrl,
268282
amountCents: totalAmountCents,
283+
subtotalCents: totalSubtotalCents,
269284
ptbInvoiceId: ptbResult.data?.[0]?.id || "",
270285
});
271286
} catch (error: any) {

valhalla/jawn/src/managers/creditsManager.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface PTBInvoice {
2929
startDate: string;
3030
endDate: string;
3131
amountCents: number;
32+
subtotalCents: number | null; // Pre-discount amount for wallet crediting (null for old invoices)
3233
notes: string | null;
3334
createdAt: string;
3435
}
@@ -311,25 +312,68 @@ export class CreditsManager extends BaseManager {
311312
*/
312313
public async listInvoices(): Promise<Result<PTBInvoice[], string>> {
313314
try {
314-
const result = await dbExecute<{
315+
// Try query with subtotal_cents column first
316+
let result = await dbExecute<{
315317
id: string;
316318
organization_id: string;
317319
stripe_invoice_id: string | null;
318320
hosted_invoice_url: string | null;
319321
start_date: string;
320322
end_date: string;
321323
amount_cents: string;
324+
subtotal_cents: string | null;
322325
notes: string | null;
323326
created_at: string;
324327
}>(
325328
`SELECT id, organization_id, stripe_invoice_id, hosted_invoice_url,
326-
start_date, end_date, amount_cents, notes, created_at
329+
start_date, end_date, amount_cents, subtotal_cents, notes, created_at
327330
FROM ptb_invoices
328331
WHERE organization_id = $1
329332
ORDER BY created_at DESC`,
330333
[this.authParams.organizationId]
331334
);
332335

336+
// If subtotal_cents column doesn't exist yet, fall back to query without it
337+
if (result.error && JSON.stringify(result.error).includes("42703")) {
338+
const fallbackResult = await dbExecute<{
339+
id: string;
340+
organization_id: string;
341+
stripe_invoice_id: string | null;
342+
hosted_invoice_url: string | null;
343+
start_date: string;
344+
end_date: string;
345+
amount_cents: string;
346+
notes: string | null;
347+
created_at: string;
348+
}>(
349+
`SELECT id, organization_id, stripe_invoice_id, hosted_invoice_url,
350+
start_date, end_date, amount_cents, notes, created_at
351+
FROM ptb_invoices
352+
WHERE organization_id = $1
353+
ORDER BY created_at DESC`,
354+
[this.authParams.organizationId]
355+
);
356+
357+
if (fallbackResult.error) {
358+
return err(`Error listing invoices: ${fallbackResult.error}`);
359+
}
360+
361+
const invoices: PTBInvoice[] = (fallbackResult.data || []).map((row) => ({
362+
id: row.id,
363+
organizationId: row.organization_id,
364+
stripeInvoiceId: row.stripe_invoice_id,
365+
hostedInvoiceUrl: row.hosted_invoice_url,
366+
startDate: row.start_date,
367+
endDate: row.end_date,
368+
amountCents: parseInt(row.amount_cents, 10),
369+
subtotalCents: null, // Column doesn't exist yet
370+
notes: row.notes,
371+
createdAt: row.created_at,
372+
}));
373+
374+
return ok(invoices);
375+
}
376+
333377
if (result.error) {
334378
return err(`Error listing invoices: ${result.error}`);
335379
}
@@ -342,6 +386,7 @@ export class CreditsManager extends BaseManager {
342386
startDate: row.start_date,
343387
endDate: row.end_date,
344388
amountCents: parseInt(row.amount_cents, 10),
389+
subtotalCents: row.subtotal_cents ? parseInt(row.subtotal_cents, 10) : null,
345390
notes: row.notes,
346391
createdAt: row.created_at,
347392
}));

valhalla/jawn/src/tsoa-build/private/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14968,6 +14968,7 @@ const models: TsoaRoute.Models = {
1496814968
"startDate": {"dataType":"string","required":true},
1496914969
"endDate": {"dataType":"string","required":true},
1497014970
"amountCents": {"dataType":"double","required":true},
14971+
"subtotalCents": {"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"required":true},
1497114972
"notes": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
1497214973
"createdAt": {"dataType":"string","required":true},
1497314974
},
@@ -15206,6 +15207,7 @@ const models: TsoaRoute.Models = {
1520615207
"hostedInvoiceUrl": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
1520715208
"dashboardUrl": {"dataType":"string","required":true},
1520815209
"amountCents": {"dataType":"double","required":true},
15210+
"subtotalCents": {"dataType":"double","required":true},
1520915211
"ptbInvoiceId": {"dataType":"string","required":true},
1521015212
},
1521115213
"additionalProperties": false,

valhalla/jawn/src/tsoa-build/private/swagger.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49208,6 +49208,11 @@
4920849208
"type": "number",
4920949209
"format": "double"
4921049210
},
49211+
"subtotalCents": {
49212+
"type": "number",
49213+
"format": "double",
49214+
"nullable": true
49215+
},
4921149216
"notes": {
4921249217
"type": "string",
4921349218
"nullable": true
@@ -49224,6 +49229,7 @@
4922449229
"startDate",
4922549230
"endDate",
4922649231
"amountCents",
49232+
"subtotalCents",
4922749233
"notes",
4922849234
"createdAt"
4922949235
],
@@ -49919,6 +49925,10 @@
4991949925
"type": "number",
4992049926
"format": "double"
4992149927
},
49928+
"subtotalCents": {
49929+
"type": "number",
49930+
"format": "double"
49931+
},
4992249932
"ptbInvoiceId": {
4992349933
"type": "string"
4992449934
}
@@ -49928,6 +49938,7 @@
4992849938
"hostedInvoiceUrl",
4992949939
"dashboardUrl",
4993049940
"amountCents",
49941+
"subtotalCents",
4993149942
"ptbInvoiceId"
4993249943
],
4993349944
"type": "object",

0 commit comments

Comments
 (0)