Skip to main content

Billing Data Model

Billing manages three stored collections (items, categories, sync jobs) and several computed report types. All stored entities use nanoid(12) identifiers. Amounts are stored in cents (integer).

BillingItem

A single cost entry.

interface BillingItem {
id: string; // nanoid(12)
name: string; // Item name
amount: number; // Amount in cents (integer)
currency: string; // Currency code (default "USD")
categoryId?: string; // Category ID
tags: string[]; // Freeform tags
date: string; // ISO 8601 date of the charge
description?: string;
provider?: string; // Cloud provider (e.g., "gcp", "aws", "openai")
providerRef?: string; // External ID for deduplication
adapter?: string; // Ingestion adapter key (e.g. "gcp-bigquery")
source?: string; // Configured source profile name
recurring: boolean; // Whether this is a recurring charge
period?: BillingPeriod; // Recurrence period
serviceId?: string; // Yellow Pages service link
featureKey?: string; // Internal feature attribution
stageAppId?: string; // Stage app ID
environment?: string; // local | stage | prod | custom
scope?: BillingScopeRef; // Generic account/workspace/project scope
usage?: BillingUsage; // Normalized usage dimensions
authorEmail?: string; // Who recorded this item
meta?: Record<string, string | number | boolean>;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}

type BillingPeriod = "daily" | "weekly" | "monthly" | "yearly";

interface BillingScopeRef {
kind: string; // e.g. "billing-account", "workspace", "subscription"
id: string;
name?: string;
}

interface BillingUsage {
amount?: number;
unit?: string;
skuId?: string;
skuDescription?: string;
}

Category

A hierarchical grouping for organizing costs.

interface Category {
id: string; // nanoid(12)
name: string; // Category name
description?: string;
color?: string; // Display color (hex)
parentId?: string; // Parent category (hierarchical)
stageAppId?: string;
authorEmail?: string;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}

SyncJob

An automated cost import operation.

interface SyncJob {
id: string; // nanoid(12)
provider: string; // Vendor family (e.g. "gcp", "aws", "openai")
adapter?: string; // Adapter name (e.g. "gcp-bigquery")
source?: string; // Configured source profile name
status: SyncJobStatus; // Job state
itemsCreated: number; // New items imported
itemsUpdated: number; // Existing items updated
startedAt: string; // ISO 8601
completedAt?: string; // ISO 8601
error?: string; // Error message on failure
stageAppId?: string;
authorEmail?: string;
}

type SyncJobStatus = "pending" | "running" | "completed" | "failed";

Report Types (computed, not stored)

BillingSummary

interface BillingSummary {
totalAmount: number; // Total in cents
itemCount: number;
currency: string;
from?: string; // ISO 8601
to?: string; // ISO 8601
}

BreakdownEntry

interface BreakdownEntry {
key: string; // Group key (provider name, category, etc.)
amount: number; // Total in cents
count: number; // Number of items
percentage: number; // Percentage of total
}

TrendBucket

interface TrendBucket {
date: string; // Bucket date (e.g., "2026-03")
amount: number; // Total in cents
count: number; // Number of items
}

StorageStats

interface StorageStats {
totalItems: number;
totalCategories: number;
totalSyncJobs: number;
totalAmount: number; // cents
currency: string;
oldestItem?: string; // ISO 8601
newestItem?: string; // ISO 8601
storageType: string;
}

BillingConfig

Billing sources are configured at the service layer, not hardcoded into the core data model.

interface BillingConfig {
version: number;
currency: string;
storage?: "file";
sources?: BillingSourceConfig[];
}

interface BillingSourceConfigBase {
name: string; // stable source/profile name
adapter: string; // adapter key, e.g. "gcp-bigquery"
provider: string; // vendor family, e.g. "gcp"
enabled?: boolean;
attribution?: BillingAttribution;
}

interface GcpBigQuerySourceConfig extends BillingSourceConfigBase {
adapter: "gcp-bigquery";
provider: "gcp";
authMode?: "auto" | "access-token" | "adc";
credentialsFile?: string; // optional service account path, or rely on ADC
datasetProjectId: string;
datasetId: string;
tableId: string;
queryProjectId?: string;
location?: string;
billingAccountId?: string;
currency?: string;
projects?: Record<string, BillingAttribution>;
}

interface BillingAttribution {
categoryId?: string;
serviceId?: string;
featureKey?: string;
stageAppId?: string;
environment?: string;
tags?: string[];
}

Storage

File-Based (SHIFT_STORAGE=file)

.billing/
config.json # BillingConfig
items/<id>.json # Individual billing items
categories/<id>.json # Category definitions
sync-jobs/<id>.json # Sync execution history

Convex (SHIFT_STORAGE=convex)

Tables use the billing_ prefix:

TableDescription
billing_itemsBilling item records
billing_categoriesCategory definitions
billing_sync_jobsSync job history

Each record includes a sid field (shift ID) that maps to the application-level id.