(params: RecordUsageParams)
| 281 | * but usage writes no longer contend on the user_stats row. |
| 282 | */ |
| 283 | export async function recordUsage(params: RecordUsageParams): Promise<void> { |
| 284 | // The usage ledger is written regardless of BILLING_ENABLED so it is the |
| 285 | // single, universal source of truth for cost (including self-hosted, where |
| 286 | // it powers the logs-page cost display). Billing *enforcement* (Stripe / |
| 287 | // overage) is gated separately by callers, not here. |
| 288 | const { |
| 289 | userId, |
| 290 | entries, |
| 291 | workspaceId, |
| 292 | workflowId, |
| 293 | executionId, |
| 294 | billingEntity, |
| 295 | billingPeriod, |
| 296 | tx, |
| 297 | } = params |
| 298 | |
| 299 | const validEntries = entries.filter((e) => e.cost > 0) |
| 300 | |
| 301 | if (validEntries.length === 0) { |
| 302 | return |
| 303 | } |
| 304 | |
| 305 | const context = await resolveBillingContext(userId, billingEntity, billingPeriod) |
| 306 | |
| 307 | const insertedRows = await (tx ?? db) |
| 308 | .insert(usageLog) |
| 309 | .values( |
| 310 | validEntries.map((entry, index) => { |
| 311 | const sourceReference = |
| 312 | entry.sourceReference ?? |
| 313 | [executionId, workflowId, workspaceId, entry.source, entry.description, index] |
| 314 | .filter((part) => part !== undefined && part !== null && part !== '') |
| 315 | .join(':') |
| 316 | const eventKey = |
| 317 | entry.eventKey ?? |
| 318 | stableEventKey({ |
| 319 | userId, |
| 320 | source: entry.source, |
| 321 | category: entry.category, |
| 322 | description: entry.description, |
| 323 | sourceReference, |
| 324 | executionId, |
| 325 | workflowId, |
| 326 | workspaceId, |
| 327 | index, |
| 328 | }) |
| 329 | |
| 330 | return { |
| 331 | id: generateId(), |
| 332 | userId, |
| 333 | category: entry.category, |
| 334 | source: entry.source, |
| 335 | description: entry.description, |
| 336 | metadata: entry.metadata ?? null, |
| 337 | cost: entry.cost.toString(), |
| 338 | eventKey, |
| 339 | billingEntityType: context.billingEntity.type, |
| 340 | billingEntityId: context.billingEntity.id, |
no test coverage detected