PhoenixKitProjects.Invoicing (PhoenixKitProjects v0.21.1)

Copy Markdown View Source

The ledger→invoice bridge (Phase E, Option B of the panel-settled design): generate a DRAFT invoice in phoenix_kit_billing from this project's uninvoiced billable time.

Boundary (non-negotiable, per the consult)

  • Billing never knows about projects: the write goes through PhoenixKitBilling.create_invoice/2 (guarded apply/3 — billing is not a dependency), with opaque source_* metadata for audit.
  • Projects owns "what's billed": the V8 phoenix_kit_project_invoiced_entries ref table (entry_uuid PK = one invoice per entry, the idempotency rule). The append-only ledger is never mutated — billed-ness DERIVES from the refs.

The honest v1

Draft only — a human reviews and issues in Billing. Bills HUMAN billable time only (kind == "time", billable == true, actor_kind in user/staff_person); AI cost needs a margin policy → v2. Minutes price via the rate_cents_per_hour config on the billing_customer extension (one money-settings home), snapshotted onto the line items at generation. Lines group by task.

Summary

Functions

Generates the draft invoice: prices the uninvoiced entries, creates the billing draft, then records the refs. Idempotent at the entry level (the PK); a refs failure after the draft exists is surfaced as {:error, :refs_failed, invoice_uuid} — the draft is real, the entries stay re-billable, and the caller tells the human to reconcile (the consult's compensating-action note).

Whether draft generation is POSSIBLE for a project: billing installed and enabled, the billing_customer extension enabled with a resolvable profile and a positive rate. Returns {:ok, %{profile:, rate_cents:}} or a tagged error the UI can phrase.

Uninvoiced billable HUMAN time entries for a project, oldest first.

Functions

generate_draft(project, opts \\ [])

@spec generate_draft(
  map(),
  keyword()
) ::
  {:ok,
   %{
     invoice_uuid: binary(),
     line_count: non_neg_integer(),
     total_cents: integer()
   }}
  | {:error, atom()}
  | {:error, :refs_failed, binary()}

Generates the draft invoice: prices the uninvoiced entries, creates the billing draft, then records the refs. Idempotent at the entry level (the PK); a refs failure after the draft exists is surfaced as {:error, :refs_failed, invoice_uuid} — the draft is real, the entries stay re-billable, and the caller tells the human to reconcile (the consult's compensating-action note).

setup(project)

@spec setup(map()) :: {:ok, map()} | {:error, atom()}

Whether draft generation is POSSIBLE for a project: billing installed and enabled, the billing_customer extension enabled with a resolvable profile and a positive rate. Returns {:ok, %{profile:, rate_cents:}} or a tagged error the UI can phrase.

uninvoiced_entries(project_uuid)

@spec uninvoiced_entries(binary()) :: [PhoenixKitProjects.Schemas.WorkEntry.t()]

Uninvoiced billable HUMAN time entries for a project, oldest first.