(sub: {
plan: string | null
referenceId: string
periodStart?: Date | null
periodEnd?: Date | null
})
| 411 | } |
| 412 | |
| 413 | export async function resetUsageForSubscription(sub: { |
| 414 | plan: string | null |
| 415 | referenceId: string |
| 416 | periodStart?: Date | null |
| 417 | periodEnd?: Date | null |
| 418 | }) { |
| 419 | const billingPeriod = |
| 420 | sub.periodStart && sub.periodEnd ? { start: sub.periodStart, end: sub.periodEnd } : null |
| 421 | |
| 422 | if (await isSubscriptionOrgScoped(sub)) { |
| 423 | const ledgerUsageByUser = billingPeriod |
| 424 | ? await getBillingPeriodUsageCostByUser( |
| 425 | { type: 'organization', id: sub.referenceId }, |
| 426 | billingPeriod |
| 427 | ) |
| 428 | : new Map<string, number>() |
| 429 | // Copilot-family ledger per user, so last-period copilot mirrors last-period |
| 430 | // cost (baseline + usage_log) instead of capturing the baseline alone. |
| 431 | const copilotLedgerByUser = billingPeriod |
| 432 | ? await getBillingPeriodUsageCostByUser( |
| 433 | { type: 'organization', id: sub.referenceId }, |
| 434 | billingPeriod, |
| 435 | COPILOT_USAGE_SOURCES |
| 436 | ) |
| 437 | : new Map<string, number>() |
| 438 | |
| 439 | await db.transaction(async (tx) => { |
| 440 | await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) |
| 441 | |
| 442 | const ownerRows = await tx |
| 443 | .select({ userId: member.userId }) |
| 444 | .from(member) |
| 445 | .where(and(eq(member.organizationId, sub.referenceId), eq(member.role, 'owner'))) |
| 446 | .for('update') |
| 447 | .limit(1) |
| 448 | |
| 449 | const ownerId = ownerRows[0]?.userId |
| 450 | if (ownerId) { |
| 451 | await tx |
| 452 | .select({ userId: userStats.userId }) |
| 453 | .from(userStats) |
| 454 | .where(eq(userStats.userId, ownerId)) |
| 455 | .for('update') |
| 456 | .limit(1) |
| 457 | } |
| 458 | |
| 459 | const membersRows = await tx |
| 460 | .select({ userId: member.userId }) |
| 461 | .from(member) |
| 462 | .where(eq(member.organizationId, sub.referenceId)) |
| 463 | |
| 464 | const memberIds = membersRows.map((row) => row.userId) |
| 465 | |
| 466 | // Lock every member's userStats before the organization row so this path |
| 467 | // follows the canonical userStats → organization order shared by the |
| 468 | // join, remove, threshold-billing, and storage-transfer paths. Locking |
| 469 | // organization first would invert against them and risk an AB-BA |
| 470 | // deadlock. The per-member UPDATE below re-locks these rows (no-op). |
no test coverage detected