Phase2: Invoicecomputation/AdminpricingUI/Ainvoicemgnt
All checks were successful
Build and Push / build (push) Successful in 1m34s
All checks were successful
Build and Push / build (push) Successful in 1m34s
This commit is contained in:
@@ -331,6 +331,12 @@ const MIGRATION_SQL = `
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
-- Phase 2 addition: per-skill one-time setup fee. Charged the
|
||||
-- first time a given (tenant, skill) appears on an invoice line.
|
||||
-- Default 0 so pricing rows created before this column exists
|
||||
-- stay free until the admin sets a fee.
|
||||
ALTER TABLE skill_pricing
|
||||
ADD COLUMN IF NOT EXISTS setup_fee_chf NUMERIC(10,2) NOT NULL DEFAULT 0;
|
||||
|
||||
-- One row per tenant. created_at anchors first-month proration;
|
||||
-- deleted_at (nullable, stamped on delete) anchors last-month
|
||||
@@ -1699,6 +1705,7 @@ function rowToSkillPricing(row: any): SkillPricing {
|
||||
return {
|
||||
skillId: row.skill_id,
|
||||
dailyPriceChf: Number(row.daily_price_chf),
|
||||
setupFeeChf: Number(row.setup_fee_chf ?? 0),
|
||||
createdAt: row.created_at?.toISOString?.() ?? row.created_at,
|
||||
updatedAt: row.updated_at?.toISOString?.() ?? row.updated_at,
|
||||
};
|
||||
@@ -1724,24 +1731,30 @@ export async function getSkillPricing(
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a daily price for a package. Setting a price activates
|
||||
* usage-based billing for the (tenant, skill) pair: every UTC day
|
||||
* the package was enabled in the billing month is one unit on the
|
||||
* invoice.
|
||||
* Upsert pricing for a package. `dailyPriceChf` activates
|
||||
* usage-based billing (one billable unit per UTC day the package
|
||||
* was enabled). `setupFeeChf` is a one-time charge emitted on the
|
||||
* first invoice line for any given (tenant, skill).
|
||||
*
|
||||
* Both fields are required so admin must consciously set 0 to mean
|
||||
* "no setup fee" rather than accidentally inheriting an old value
|
||||
* from a partial update.
|
||||
*/
|
||||
export async function setSkillPricing(
|
||||
skillId: string,
|
||||
dailyPriceChf: number
|
||||
dailyPriceChf: number,
|
||||
setupFeeChf: number
|
||||
): Promise<SkillPricing> {
|
||||
await ensureSchema();
|
||||
const result = await getPool().query(
|
||||
`INSERT INTO skill_pricing (skill_id, daily_price_chf)
|
||||
VALUES ($1, $2)
|
||||
`INSERT INTO skill_pricing (skill_id, daily_price_chf, setup_fee_chf)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (skill_id) DO UPDATE SET
|
||||
daily_price_chf = EXCLUDED.daily_price_chf,
|
||||
setup_fee_chf = EXCLUDED.setup_fee_chf,
|
||||
updated_at = now()
|
||||
RETURNING *`,
|
||||
[skillId, dailyPriceChf]
|
||||
[skillId, dailyPriceChf, setupFeeChf]
|
||||
);
|
||||
return rowToSkillPricing(result.rows[0]);
|
||||
}
|
||||
@@ -2500,6 +2513,32 @@ export async function tenantHasSetupFeeBilled(
|
||||
return result.rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has this (tenant, skill) pair already appeared on any prior
|
||||
* invoice line — either as setup or usage? Drives the per-skill
|
||||
* setup-fee gate. Same "first appearance" semantics as the tenant
|
||||
* setup fee: a previously-free skill that newly gets a setup fee
|
||||
* configured will trigger the fee on its next billed period.
|
||||
*
|
||||
* Uses metadata->>'skill_id' (which is what both skill_setup and
|
||||
* skill_usage lines store) rather than parsing description.
|
||||
*/
|
||||
export async function tenantSkillHasBeenBilled(
|
||||
tenantName: string,
|
||||
skillId: string
|
||||
): Promise<boolean> {
|
||||
await ensureSchema();
|
||||
const result = await getPool().query(
|
||||
`SELECT 1 FROM invoice_lines
|
||||
WHERE tenant_name = $1
|
||||
AND kind IN ('skill_setup', 'skill_usage')
|
||||
AND metadata->>'skill_id' = $2
|
||||
LIMIT 1`,
|
||||
[tenantName, skillId]
|
||||
);
|
||||
return result.rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate open balance per org for the admin overview. Returns
|
||||
* orgs with at least one open or overdue invoice; orgs in good
|
||||
|
||||
Reference in New Issue
Block a user