diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index 4e2ac7b..ef57c91 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ "./src/schema/github.ts", "./src/schema/events.ts", "./src/schema/contribution.ts", + "./src/schema/ledger.ts", ], out: "./migrations", dbCredentials: { url: process.env["DATABASE_URL"] ?? "" }, diff --git a/packages/database/migrations/0005_tricky_random.sql b/packages/database/migrations/0005_tricky_random.sql new file mode 100644 index 0000000..6c5ba22 --- /dev/null +++ b/packages/database/migrations/0005_tricky_random.sql @@ -0,0 +1,135 @@ +CREATE TYPE "public"."account_type" AS ENUM('CENTRAL_BANK_RESERVE', 'GLOBAL_WALLET', 'ORGANIZATION_POSITION', 'TREASURY', 'REVIEW_FUND', 'PENDING', 'NETWORK_RESERVE', 'PROTOCOL', 'BURNED');--> statement-breakpoint +CREATE TYPE "public"."currency_type" AS ENUM('KRED', 'LOCAL');--> statement-breakpoint +CREATE TYPE "public"."economy_type" AS ENUM('KREDS_NETWORK', 'SOVEREIGN_NETWORK', 'INDEPENDENT');--> statement-breakpoint +CREATE TYPE "public"."entry_direction" AS ENUM('DEBIT', 'CREDIT', 'MEMO');--> statement-breakpoint +CREATE TYPE "public"."entry_source_type" AS ENUM('PULL_REQUEST_MERGED', 'PULL_REQUEST_CLOSED', 'REVIEW_SUBMITTED', 'SETTLEMENT_RUN', 'TREASURY_OPERATION', 'CREDIT_OPERATION', 'NETWORK_OPERATION', 'MANUAL_ADJUSTMENT');--> statement-breakpoint +CREATE TYPE "public"."entry_status" AS ENUM('PENDING', 'SETTLED');--> statement-breakpoint +CREATE TYPE "public"."transaction_type" AS ENUM('DISTRIBUTION', 'TRANSFER', 'FEE', 'REFUND', 'REVERSAL', 'TREASURY_CONTRIBUTION', 'TREASURY_DISTRIBUTION', 'BURN', 'ADJUSTMENT', 'RESERVE_ALLOCATION', 'EXCHANGE', 'SETTLEMENT', 'REVIEW_FUND_CONTRIBUTION', 'REVIEW_FUND_PAYMENT', 'CREDIT_DRAW', 'DEBT_REPAYMENT', 'RECEIVABLE_CREATED', 'RECEIVABLE_SETTLED', 'RECEIVABLE_CANCELLED');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "accounts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "economy_id" uuid NOT NULL, + "type" "account_type" NOT NULL, + "owner_github_user_id" bigint, + "organization_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "currencies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "economy_id" uuid NOT NULL, + "type" "currency_type" NOT NULL, + "code" text NOT NULL, + "name" text NOT NULL, + "subunits_per_unit" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "economies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "type" "economy_type" NOT NULL, + "organization_id" uuid, + "name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ledger_entries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "transaction_id" uuid NOT NULL, + "economy_id" uuid NOT NULL, + "organization_id" uuid, + "account_id" uuid NOT NULL, + "direction" "entry_direction" NOT NULL, + "amount" bigint NOT NULL, + "type" "transaction_type" NOT NULL, + "source_type" "entry_source_type" NOT NULL, + "source_id" text NOT NULL, + "counterparty_account_id" uuid, + "rules_version" text NOT NULL, + "idempotency_key" text NOT NULL, + "status" "entry_status" DEFAULT 'PENDING' NOT NULL, + "settled_at" timestamp with time zone, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ledger_transactions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "economy_id" uuid NOT NULL, + "type" "transaction_type" NOT NULL, + "idempotency_key" text NOT NULL, + "rules_version" text NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ledger_transactions_idempotency_key_unique" UNIQUE("idempotency_key") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "accounts" ADD CONSTRAINT "accounts_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "accounts" ADD CONSTRAINT "accounts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "currencies" ADD CONSTRAINT "currencies_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "economies" ADD CONSTRAINT "economies_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_transaction_id_ledger_transactions_id_fk" FOREIGN KEY ("transaction_id") REFERENCES "public"."ledger_transactions"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_counterparty_account_id_accounts_id_fk" FOREIGN KEY ("counterparty_account_id") REFERENCES "public"."accounts"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_transactions" ADD CONSTRAINT "ledger_transactions_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "accounts_economy_owner_idx" ON "accounts" USING btree ("economy_id","owner_github_user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "accounts_economy_type_idx" ON "accounts" USING btree ("economy_id","type");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "accounts_organization_idx" ON "accounts" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "currencies_economy_idx" ON "currencies" USING btree ("economy_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "economies_organization_idx" ON "economies" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_account_idx" ON "ledger_entries" USING btree ("account_id","status");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_transaction_idx" ON "ledger_entries" USING btree ("transaction_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_economy_idx" ON "ledger_entries" USING btree ("economy_id","created_at");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_source_idx" ON "ledger_entries" USING btree ("source_type","source_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_transactions_economy_idx" ON "ledger_transactions" USING btree ("economy_id","created_at"); \ No newline at end of file diff --git a/packages/database/migrations/meta/0005_snapshot.json b/packages/database/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..9d4ee37 --- /dev/null +++ b/packages/database/migrations/meta/0005_snapshot.json @@ -0,0 +1,1730 @@ +{ + "id": "5e432fbf-2005-468f-a83a-dd42937daa5d", + "prevId": "74fa5157-8999-4dd1-b11d-dbcef7d0adce", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.github_identities": { + "name": "github_identities", + "schema": "", + "columns": { + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'UNKNOWN'" + }, + "status": { + "name": "status", + "type": "identity_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'UNCLAIMED'" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_identities_user_id_idx": { + "name": "github_identities_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_identities_login_idx": { + "name": "github_identities_login_idx", + "columns": [ + { + "expression": "login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_identities_user_id_users_id_fk": { + "name": "github_identities_user_id_users_id_fk", + "tableFrom": "github_identities", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installations": { + "name": "installations", + "schema": "", + "columns": { + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "installation_account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_github_id": { + "name": "account_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "installation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "installations_organization_id_idx": { + "name": "installations_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "installations_organization_id_organizations_id_fk": { + "name": "installations_organization_id_organizations_id_fk", + "tableFrom": "installations", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_organization_id": { + "name": "github_organization_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_login_idx": { + "name": "organizations_login_idx", + "columns": [ + { + "expression": "login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_github_organization_id_unique": { + "name": "organizations_github_organization_id_unique", + "nullsNotDistinct": false, + "columns": ["github_organization_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name_with_owner": { + "name": "name_with_owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "is_personally_owned": { + "name": "is_personally_owned", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "trust_tier": { + "name": "trust_tier", + "type": "repository_trust_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'UNTRUSTED'" + }, + "primary_branch": { + "name": "primary_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "relevance_score": { + "name": "relevance_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "relevance_breadth": { + "name": "relevance_breadth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "relevance_signals": { + "name": "relevance_signals", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "relevance_measured_at": { + "name": "relevance_measured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "repositories_installation_id_idx": { + "name": "repositories_installation_id_idx", + "columns": [ + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositories_organization_id_idx": { + "name": "repositories_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositories_name_with_owner_idx": { + "name": "repositories_name_with_owner_idx", + "columns": [ + { + "expression": "name_with_owner", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repositories_github_installation_id_installations_github_installation_id_fk": { + "name": "repositories_github_installation_id_installations_github_installation_id_fk", + "tableFrom": "repositories", + "tableTo": "installations", + "columnsFrom": ["github_installation_id"], + "columnsTo": ["github_installation_id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "repositories_organization_id_organizations_id_fk": { + "name": "repositories_organization_id_organizations_id_fk", + "tableFrom": "repositories", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_github_repository_id_unique": { + "name": "repositories_github_repository_id_unique", + "nullsNotDistinct": false, + "columns": ["github_repository_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain_events": { + "name": "domain_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "domain_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "github_event_id": { + "name": "github_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "domain_events_repository_idx": { + "name": "domain_events_repository_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_events_type_idx": { + "name": "domain_events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domain_events_github_event_id_github_events_id_fk": { + "name": "domain_events_github_event_id_github_events_id_fk", + "tableFrom": "domain_events", + "tableTo": "github_events", + "columnsFrom": ["github_event_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "domain_events_repository_id_repositories_id_fk": { + "name": "domain_events_repository_id_repositories_id_fk", + "tableFrom": "domain_events", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domain_events_idempotency_key_unique": { + "name": "domain_events_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_events": { + "name": "github_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_delivery_id": { + "name": "github_delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'RECEIVED'" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_events_status_idx": { + "name": "github_events_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_events_installation_idx": { + "name": "github_events_installation_idx", + "columns": [ + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_events_type_idx": { + "name": "github_events_type_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_events_github_delivery_id_unique": { + "name": "github_events_github_delivery_id_unique", + "nullsNotDistinct": false, + "columns": ["github_delivery_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contribution_entries": { + "name": "contribution_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "contribution_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "contribution_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quality_score": { + "name": "quality_score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "unobserved_signals": { + "name": "unobserved_signals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "invalidation_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cancels_entry_id": { + "name": "cancels_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rules_version": { + "name": "rules_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contribution_entries_user_idx": { + "name": "contribution_entries_user_idx", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contribution_entries_org_idx": { + "name": "contribution_entries_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contribution_entries_repository_idx": { + "name": "contribution_entries_repository_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contribution_entries_repository_id_repositories_id_fk": { + "name": "contribution_entries_repository_id_repositories_id_fk", + "tableFrom": "contribution_entries", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "contribution_entries_organization_id_organizations_id_fk": { + "name": "contribution_entries_organization_id_organizations_id_fk", + "tableFrom": "contribution_entries", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contribution_entries_idempotency_key_unique": { + "name": "contribution_entries_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "owner_github_user_id": { + "name": "owner_github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_economy_owner_idx": { + "name": "accounts_economy_owner_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_economy_type_idx": { + "name": "accounts_economy_type_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_organization_idx": { + "name": "accounts_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_economy_id_economies_id_fk": { + "name": "accounts_economy_id_economies_id_fk", + "tableFrom": "accounts", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "accounts_organization_id_organizations_id_fk": { + "name": "accounts_organization_id_organizations_id_fk", + "tableFrom": "accounts", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.currencies": { + "name": "currencies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "currency_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subunits_per_unit": { + "name": "subunits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "currencies_economy_idx": { + "name": "currencies_economy_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "currencies_economy_id_economies_id_fk": { + "name": "currencies_economy_id_economies_id_fk", + "tableFrom": "currencies", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.economies": { + "name": "economies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "economy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "economies_organization_idx": { + "name": "economies_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "economies_organization_id_organizations_id_fk": { + "name": "economies_organization_id_organizations_id_fk", + "tableFrom": "economies", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_entries": { + "name": "ledger_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transaction_id": { + "name": "transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "entry_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "entry_source_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counterparty_account_id": { + "name": "counterparty_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rules_version": { + "name": "rules_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "entry_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_entries_account_idx": { + "name": "ledger_entries_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_transaction_idx": { + "name": "ledger_entries_transaction_idx", + "columns": [ + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_economy_idx": { + "name": "ledger_entries_economy_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_source_idx": { + "name": "ledger_entries_source_idx", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_entries_transaction_id_ledger_transactions_id_fk": { + "name": "ledger_entries_transaction_id_ledger_transactions_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "ledger_transactions", + "columnsFrom": ["transaction_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ledger_entries_economy_id_economies_id_fk": { + "name": "ledger_entries_economy_id_economies_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ledger_entries_organization_id_organizations_id_fk": { + "name": "ledger_entries_organization_id_organizations_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_account_id_accounts_id_fk": { + "name": "ledger_entries_account_id_accounts_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "accounts", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ledger_entries_counterparty_account_id_accounts_id_fk": { + "name": "ledger_entries_counterparty_account_id_accounts_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "accounts", + "columnsFrom": ["counterparty_account_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_transactions": { + "name": "ledger_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules_version": { + "name": "rules_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_transactions_economy_idx": { + "name": "ledger_transactions_economy_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_transactions_economy_id_economies_id_fk": { + "name": "ledger_transactions_economy_id_economies_id_fk", + "tableFrom": "ledger_transactions", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ledger_transactions_idempotency_key_unique": { + "name": "ledger_transactions_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.actor_type": { + "name": "actor_type", + "schema": "public", + "values": ["HUMAN", "BOT", "AI_AGENT", "UNKNOWN"] + }, + "public.identity_status": { + "name": "identity_status", + "schema": "public", + "values": ["CLAIMED", "UNCLAIMED", "RESTRICTED"] + }, + "public.installation_account_type": { + "name": "installation_account_type", + "schema": "public", + "values": ["ORGANIZATION", "USER"] + }, + "public.installation_status": { + "name": "installation_status", + "schema": "public", + "values": ["ACTIVE", "SUSPENDED", "REMOVED"] + }, + "public.repository_trust_tier": { + "name": "repository_trust_tier", + "schema": "public", + "values": ["UNTRUSTED", "ESTABLISHED", "RELEVANT", "HIGH_TRUST"] + }, + "public.domain_event_type": { + "name": "domain_event_type", + "schema": "public", + "values": [ + "PULL_REQUEST_MERGED", + "PULL_REQUEST_CLOSED", + "REVIEW_SUBMITTED", + "REPOSITORY_CONNECTED", + "REPOSITORY_DISCONNECTED" + ] + }, + "public.event_status": { + "name": "event_status", + "schema": "public", + "values": ["RECEIVED", "QUEUED", "PROCESSING", "PROCESSED", "FAILED", "IGNORED"] + }, + "public.contribution_entry_type": { + "name": "contribution_entry_type", + "schema": "public", + "values": ["AWARD", "INVALIDATION"] + }, + "public.contribution_kind": { + "name": "contribution_kind", + "schema": "public", + "values": ["PULL_REQUEST_MERGED", "CODE_REVIEW", "ISSUE_RESOLVED", "REVIEW_FOLLOW_UP"] + }, + "public.invalidation_trigger": { + "name": "invalidation_trigger", + "schema": "public", + "values": [ + "PR_REVERTED", + "CONFIRMED_FRAUD", + "CONFIRMED_FARMING", + "ACTOR_RECLASSIFIED_NON_HUMAN" + ] + }, + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "CENTRAL_BANK_RESERVE", + "GLOBAL_WALLET", + "ORGANIZATION_POSITION", + "TREASURY", + "REVIEW_FUND", + "PENDING", + "NETWORK_RESERVE", + "PROTOCOL", + "BURNED" + ] + }, + "public.currency_type": { + "name": "currency_type", + "schema": "public", + "values": ["KRED", "LOCAL"] + }, + "public.economy_type": { + "name": "economy_type", + "schema": "public", + "values": ["KREDS_NETWORK", "SOVEREIGN_NETWORK", "INDEPENDENT"] + }, + "public.entry_direction": { + "name": "entry_direction", + "schema": "public", + "values": ["DEBIT", "CREDIT", "MEMO"] + }, + "public.entry_source_type": { + "name": "entry_source_type", + "schema": "public", + "values": [ + "PULL_REQUEST_MERGED", + "PULL_REQUEST_CLOSED", + "REVIEW_SUBMITTED", + "SETTLEMENT_RUN", + "TREASURY_OPERATION", + "CREDIT_OPERATION", + "NETWORK_OPERATION", + "MANUAL_ADJUSTMENT" + ] + }, + "public.entry_status": { + "name": "entry_status", + "schema": "public", + "values": ["PENDING", "SETTLED"] + }, + "public.transaction_type": { + "name": "transaction_type", + "schema": "public", + "values": [ + "DISTRIBUTION", + "TRANSFER", + "FEE", + "REFUND", + "REVERSAL", + "TREASURY_CONTRIBUTION", + "TREASURY_DISTRIBUTION", + "BURN", + "ADJUSTMENT", + "RESERVE_ALLOCATION", + "EXCHANGE", + "SETTLEMENT", + "REVIEW_FUND_CONTRIBUTION", + "REVIEW_FUND_PAYMENT", + "CREDIT_DRAW", + "DEBT_REPAYMENT", + "RECEIVABLE_CREATED", + "RECEIVABLE_SETTLED", + "RECEIVABLE_CANCELLED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 2b97428..910193e 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1787413331784, "tag": "0004_hesitant_moon_knight", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1787414132672, + "tag": "0005_tricky_random", + "breakpoints": true } ] } diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 367ebf5..a4a1cff 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -32,5 +32,6 @@ export { type InvalidationTrigger, type LeaderboardRow, } from "./repositories/contribution-ledger.js"; +export { InsufficientBalanceError, Ledger, type PostedTransaction } from "./repositories/ledger.js"; export { runMigrations } from "./migrate.js"; export * as schema from "./schema/index.js"; diff --git a/packages/database/src/repositories/ledger.test.ts b/packages/database/src/repositories/ledger.test.ts new file mode 100644 index 0000000..5729d98 --- /dev/null +++ b/packages/database/src/repositories/ledger.test.ts @@ -0,0 +1,402 @@ +import { afterAll, beforeEach, describe, expect, it } from "vitest"; +import { sql } from "drizzle-orm"; +import { dirname, join } from "node:path"; + +import { + accountId as toAccountId, + economyId as toEconomyId, + fromIso, + idempotencyKey as toKey, + kredbits, + ledgerEntryId as toEntryId, + rulesVersion as toRulesVersion, + transactionId as toTransactionId, + type LedgerEntry, + type Transaction, +} from "@kreds/domain"; + +import { createDatabase, type Database } from "../client.js"; +import { runMigrations } from "../migrate.js"; +import { InsufficientBalanceError, Ledger } from "./ledger.js"; + +const url = process.env["DATABASE_URL"]; +const describeWithDatabase = url ? describe : describe.skip; + +let db: Database; +let ledger: Ledger; + +const ECONOMY = "20000000-0000-0000-0000-000000000001"; +const RESERVE = "30000000-0000-0000-0000-000000000001"; +const ISAAC = "30000000-0000-0000-0000-000000000002"; +const JOSE = "30000000-0000-0000-0000-000000000003"; + +function entryOf(over: Partial & Pick) { + return { + id: toEntryId(crypto.randomUUID()), + economyId: toEconomyId(ECONOMY), + organizationId: null, + amount: kredbits(0n), + type: "TRANSFER" as const, + sourceType: "PULL_REQUEST_MERGED" as const, + sourceId: "PULL_REQUEST_MERGED:77001:412", + counterpartyAccountId: null, + rulesVersion: toRulesVersion("v0.4"), + idempotencyKey: toKey("entry"), + status: "SETTLED" as const, + settledAt: fromIso("2026-08-22T10:00:00Z"), + createdAt: fromIso("2026-08-22T10:00:00Z"), + metadata: {}, + ...over, + } as LedgerEntry; +} + +/** A two-sided movement: `from` is debited, `to` is credited. */ +function movement( + key: string, + from: string, + to: string, + amount: bigint, + over: Partial = {}, +): Transaction { + // The domain requires an entry to carry its transaction's type, so a + // distribution cannot contain an entry that reads as a transfer. + const type = (over.type ?? "TRANSFER") as Transaction["type"]; + return { + id: toTransactionId(crypto.randomUUID()), + type, + economyId: toEconomyId(ECONOMY), + rulesVersion: toRulesVersion("v0.4"), + idempotencyKey: toKey(key), + createdAt: fromIso("2026-08-22T10:00:00Z"), + entries: [ + entryOf({ + accountId: toAccountId(from), + direction: "DEBIT", + amount: kredbits(amount), + counterpartyAccountId: toAccountId(to), + // The domain requires every entry to carry its transaction's key, so + // one half of a movement cannot be deduplicated apart from the other. + idempotencyKey: toKey(key), + type, + }), + entryOf({ + accountId: toAccountId(to), + direction: "CREDIT", + amount: kredbits(amount), + counterpartyAccountId: toAccountId(from), + idempotencyKey: toKey(key), + type, + }), + ], + ...over, + } as Transaction; +} + +describeWithDatabase("Ledger", () => { + beforeEach(async () => { + db ??= createDatabase({ url: url as string, max: 4 }); + const here = dirname(new URL(import.meta.url).pathname); + await runMigrations(url as string, join(here, "..", "..", "migrations")); + await db.execute( + sql`truncate table ledger_entries, ledger_transactions, accounts, currencies, economies cascade`, + ); + await db.execute( + sql`insert into economies (id, type, name) values (${ECONOMY}, 'INDEPENDENT', 'test')`, + ); + for (const [id, type] of [ + [RESERVE, "CENTRAL_BANK_RESERVE"], + [ISAAC, "GLOBAL_WALLET"], + [JOSE, "GLOBAL_WALLET"], + ] as const) { + await db.execute( + sql`insert into accounts (id, economy_id, type) values (${id}, ${ECONOMY}, ${type})`, + ); + } + ledger = new Ledger(db); + }); + + afterAll(async () => { + await db?.$client.end({ timeout: 5 }); + }); + + describe("balances are derived", () => { + /** + * The single most consequential decision in the system, checked directly. + * There is no balance column, so there is nothing to read but the entries. + */ + it("has no balance column to read", async () => { + const columns = await db.execute<{ column_name: string }>( + sql`select column_name from information_schema.columns where table_name = 'accounts'`, + ); + expect(columns.map((c) => c.column_name)).not.toContain("balance"); + }); + + it("derives a balance from the entries that produced it", async () => { + await seed(3_500n); + expect(await ledger.balanceOf(ISAAC)).toBe(3_500n); + expect(await ledger.balanceOf(RESERVE)).toBe(10_000n - 3_500n); + }); + + it("reports zero for an account nothing has touched", async () => { + expect(await ledger.balanceOf(JOSE)).toBe(0n); + }); + }); + + describe("every movement balances", () => { + it("posts a two-sided transfer", async () => { + await seed(1_000n); + const posted = await ledger.post(movement("t-1", ISAAC, JOSE, 400n)); + + expect(posted.isNew).toBe(true); + expect(await ledger.balanceOf(ISAAC)).toBe(600n); + expect(await ledger.balanceOf(JOSE)).toBe(400n); + }); + + it("refuses entries that do not sum to zero, before touching the database", async () => { + const lopsided = movement("t-bad", ISAAC, JOSE, 100n); + const broken = { + ...lopsided, + entries: [ + lopsided.entries[0], + entryOf({ + accountId: toAccountId(JOSE), + direction: "CREDIT", + amount: kredbits(999n), + }), + ], + } as Transaction; + + await expect(ledger.post(broken)).rejects.toThrow(); + const [count] = await db.execute<{ count: string }>( + sql`select count(*)::text as count from ledger_transactions where idempotency_key = 't-bad'`, + ); + expect(count?.count).toBe("0"); + }); + + it("keeps the entries of a transaction retrievable, so it can be audited", async () => { + await seed(1_000n); + const posted = await ledger.post(movement("t-2", ISAAC, JOSE, 250n)); + const entries = await ledger.entriesOf(posted.id); + + expect(entries).toHaveLength(2); + const net = entries.reduce( + (sum, e) => sum + (e.direction === "CREDIT" ? e.amount : -e.amount), + 0n, + ); + expect(net).toBe(0n); + }); + }); + + describe("Law XXI, no monetary creation through debt", () => { + /** + * 06: "an implementation that can produce a negative balance can mint + * currency." This is the test that says it cannot. + */ + it("refuses a movement that would overdraw an account", async () => { + await seed(100n); + await expect(ledger.post(movement("t-over", ISAAC, JOSE, 500n))).rejects.toBeInstanceOf( + InsufficientBalanceError, + ); + }); + + /** The rollback matters as much as the refusal: a half-written movement is worse. */ + it("writes nothing at all when it refuses", async () => { + await seed(100n); + await ledger.post(movement("t-over2", ISAAC, JOSE, 500n)).catch(() => undefined); + + expect(await ledger.balanceOf(ISAAC)).toBe(100n); + expect(await ledger.balanceOf(JOSE)).toBe(0n); + const [count] = await db.execute<{ count: string }>( + sql`select count(*)::text as count from ledger_entries where source_id like '%412%' and transaction_id in (select id from ledger_transactions where idempotency_key = 't-over2')`, + ); + expect(count?.count).toBe("0"); + }); + + it("allows a movement that lands exactly on zero", async () => { + await seed(400n); + await ledger.post(movement("t-exact", ISAAC, JOSE, 400n)); + expect(await ledger.balanceOf(ISAAC)).toBe(0n); + }); + + /** + * The concurrency the lock exists for. Two debits that each look affordable + * alone must not both succeed: that is the path 06 says mints currency. + */ + it("does not let two concurrent debits together overdraw an account", async () => { + await seed(500n); + + const results = await Promise.allSettled([ + ledger.post(movement("race-a", ISAAC, JOSE, 400n)), + ledger.post(movement("race-b", ISAAC, JOSE, 400n)), + ]); + + const succeeded = results.filter((r) => r.status === "fulfilled").length; + expect(succeeded).toBe(1); + expect(await ledger.balanceOf(ISAAC)).toBe(100n); + }); + }); + + describe("idempotency", () => { + /** GitHub delivers at least once, so the same movement arrives more than once. */ + it("posts the same movement once", async () => { + await seed(1_000n); + const first = await ledger.post(movement("t-idem", ISAAC, JOSE, 300n)); + const again = await ledger.post(movement("t-idem", ISAAC, JOSE, 300n)); + + expect(again.isNew).toBe(false); + expect(again.id).toBe(first.id); + expect(await ledger.balanceOf(JOSE)).toBe(300n); + }); + + it("survives ten replays without paying ten times", async () => { + await seed(1_000n); + for (let i = 0; i < 10; i++) { + await ledger.post(movement("t-replay", ISAAC, JOSE, 250n)); + } + expect(await ledger.balanceOf(JOSE)).toBe(250n); + }); + }); + + describe("what settled and what merely happened", () => { + /** Law VII: earned is not withdrawable. */ + it("counts a pending entry in the balance but not in what settled", async () => { + await seed(1_000n); + await ledger.post( + movement("t-pending", ISAAC, JOSE, 200n, { + entries: [ + entryOf({ + accountId: toAccountId(ISAAC), + direction: "DEBIT", + amount: kredbits(200n), + status: "PENDING", + settledAt: null, + idempotencyKey: toKey("t-pending"), + }), + entryOf({ + accountId: toAccountId(JOSE), + direction: "CREDIT", + amount: kredbits(200n), + status: "PENDING", + settledAt: null, + idempotencyKey: toKey("t-pending"), + }), + ], + } as Partial), + ); + + expect(await ledger.balanceOf(JOSE)).toBe(200n); + expect(await ledger.settledBalanceOf(JOSE)).toBe(0n); + }); + }); + + describe("precision", () => { + /** + * 06: "Floating point silently loses value." A value past the safe integer + * range must round-trip exactly, which it cannot do through a JavaScript + * number. + */ + it("round-trips a value no double could hold", async () => { + const huge = 9_007_199_254_740_993n; // MAX_SAFE_INTEGER + 2 + await seedAmount(huge); + expect(await ledger.balanceOf(ISAAC)).toBe(huge); + }); + + it("never loses a subunit across many small movements", async () => { + await seed(1_000n); + for (let i = 0; i < 20; i++) { + await ledger.post(movement(`t-small-${i}`, ISAAC, JOSE, 7n)); + } + expect(await ledger.balanceOf(ISAAC)).toBe(1_000n - 140n); + expect(await ledger.balanceOf(JOSE)).toBe(140n); + // Conservation: what left one account arrived at the other, exactly. + expect((await ledger.balanceOf(ISAAC)) + (await ledger.balanceOf(JOSE))).toBe(1_000n); + }); + }); + + describe("history explains the balance", () => { + /** + * 06: "If a support ticket asks 'why do I have 1,240 K?', the answer must be + * reconstructible from entries, not from a support engineer's memory." + */ + it("reconstructs a balance from nothing but its entries", async () => { + await seed(1_000n); + await ledger.post(movement("h-1", ISAAC, JOSE, 300n)); + await ledger.post(movement("h-2", ISAAC, JOSE, 120n)); + + const entries = await ledger.entriesFor(ISAAC); + const reconstructed = entries.reduce( + (sum, e) => sum + (e.direction === "CREDIT" ? e.amount : -e.amount), + 0n, + ); + + expect(reconstructed).toBe(await ledger.balanceOf(ISAAC)); + expect(reconstructed).toBe(580n); + }); + + /** No update, no delete: history is repaired with new entries, never in place. */ + it("has exactly one way to write, and none to change or remove", () => { + // The exact surface, rather than a pattern. A regex here first flagged + // settledBalanceOf, which reads, and a guard that cries wolf is a guard + // somebody relaxes. Listing what may exist says the same thing and + // cannot be fooled by a name that merely looks like a verb. + const surface = Object.getOwnPropertyNames(Ledger.prototype) + .filter((name) => name !== "constructor" && !name.startsWith("_")) + .sort(); + + expect(surface).toEqual([ + "balanceOf", + "entriesFor", + "entriesOf", + "findByIdempotencyKey", + "netAmount", + "post", + "rawBalance", + "settledBalanceOf", + ]); + }); + }); + + /** + * Give the reserve an opening balance, directly. + * + * Not through `post`, and that is the finding rather than a shortcut: a + * transaction must sum to zero, and creating the money supply is precisely + * the movement that does not. Genesis belongs to the Central Bank, which is + * Phase 8. This fixture stands in for it so the ledger's own guarantees can + * be tested against something that already exists. + */ + async function openingBalance(amount: bigint): Promise { + await db.execute(sql` + insert into ledger_transactions (id, economy_id, type, idempotency_key, rules_version) + values (gen_random_uuid(), ${ECONOMY}, 'DISTRIBUTION', 'genesis', 'v0.4') + `); + await db.execute(sql` + insert into ledger_entries + (transaction_id, economy_id, account_id, direction, amount, type, + source_type, source_id, rules_version, idempotency_key, status, settled_at) + select id, ${ECONOMY}, ${RESERVE}, 'CREDIT', ${amount.toString()}::bigint, 'DISTRIBUTION', + 'NETWORK_OPERATION', 'genesis', 'v0.4', 'genesis-entry', 'SETTLED', now() + from ledger_transactions where idempotency_key = 'genesis' + `); + } + + /** Open the reserve, then issue from it to Isaac. */ + async function seed(amount: bigint): Promise { + await openingBalance(10_000n); + await ledger.post( + movement("seed-issue", RESERVE, ISAAC, amount, { + type: "DISTRIBUTION", + } as Partial), + ); + } + + /** The same, when the amount is larger than any sensible float. */ + async function seedAmount(amount: bigint): Promise { + await openingBalance(amount * 2n); + await ledger.post( + movement("seed-issue", RESERVE, ISAAC, amount, { + type: "DISTRIBUTION", + } as Partial), + ); + } +}); diff --git a/packages/database/src/repositories/ledger.ts b/packages/database/src/repositories/ledger.ts new file mode 100644 index 0000000..dd13f14 --- /dev/null +++ b/packages/database/src/repositories/ledger.ts @@ -0,0 +1,255 @@ +import { and, eq, inArray, sql } from "drizzle-orm"; +import { + kredbits, + transaction as validateTransaction, + type Kredbits, + type Transaction, +} from "@kreds/domain"; + +import type { Database } from "../client.js"; +import { accounts, ledgerEntries, ledgerTransactions } from "../schema/index.js"; + +/** + * Either the pool or an open transaction. + * + * The balance check has to run *inside* the transaction that wrote the entries, + * or it would read a state the rest of the world can already see and the whole + * lock would be decorative. + */ +type Queryable = Pick; + +/** + * Thrown when a movement would take an account below zero. + * + * Law XXI, No Monetary Creation Through Debt: "A negative economic position may + * represent a liability, but it may never create spendable Official KRED." Debt + * is a separate field, never a negative balance, and 06 is explicit that "an + * implementation that can produce a negative balance can mint currency." + * + * Its own error type rather than a generic one, because a caller has to be able + * to tell an overdraft from a database failure: the first is a decision the + * economy made and the second is an outage. + */ +export class InsufficientBalanceError extends Error { + constructor( + readonly accountId: string, + readonly shortfall: Kredbits, + ) { + super(`account ${accountId} would go negative by ${shortfall} subunits.`); + this.name = "InsufficientBalanceError"; + } +} + +export interface PostedTransaction { + readonly id: string; + readonly idempotencyKey: string; + /** False when this movement had already been posted. */ + readonly isNew: boolean; +} + +/** + * The ledger. + * + * 06 calls ledger-first "the single most consequential engineering decision in + * the system, and the one most likely to be compromised for convenience", so + * the shortcuts it warns about are made unavailable rather than discouraged: + * + * - There is no balance column, so a balance cannot be stored. + * - There is no method that writes a single entry, so nothing can post one side + * of a movement. + * - There is no update or delete, so history cannot be repaired in place. + * + * What remains is `post`, which writes a balanced transaction or writes nothing. + */ +export class Ledger { + constructor(private readonly db: Database) {} + + /** + * Post a balanced transaction, atomically. + * + * Three things have to hold, and all three are checked before anything is + * visible to anybody: + * + * 1. **The entries balance.** Validated by `@kreds/domain`, which enforces it + * per economy so that only an `EXCHANGE` may span two (Law X). + * 2. **No account goes negative.** Checked after the entries are written and + * inside the same database transaction, with the affected accounts locked + * first so a concurrent debit cannot slip between the check and the write. + * 3. **The same movement posts once.** The idempotency key is unique, and a + * repeat returns the original rather than raising. + * + * @throws InsufficientBalanceError when the movement would overdraw an + * account. The database transaction rolls back, so nothing is written. + */ + async post(candidate: Transaction): Promise { + // Balance and economy rules first, before touching the database. A + // malformed transaction should never reach a connection. + const validated = validateTransaction(candidate); + + const existing = await this.findByIdempotencyKey(validated.idempotencyKey); + if (existing) return { ...existing, isNew: false }; + + return this.db.transaction(async (tx) => { + const affected = [...new Set(validated.entries.map((entry) => entry.accountId))]; + + // Lock every affected account for the rest of this transaction. Without + // this, two concurrent debits can each read a sufficient balance, each + // pass the check, and together overdraw the account, which is the exact + // path 06 says can mint currency. + await tx + .select({ id: accounts.id }) + .from(accounts) + .where(inArray(accounts.id, affected)) + .for("update"); + + const [posted] = await tx + .insert(ledgerTransactions) + .values({ + economyId: validated.economyId, + type: validated.type, + idempotencyKey: validated.idempotencyKey, + rulesVersion: validated.rulesVersion, + metadata: null, + }) + .onConflictDoNothing({ target: ledgerTransactions.idempotencyKey }) + .returning(); + + if (!posted) { + // Another connection posted the same movement between the check above + // and this insert. That is the idempotency working, not a failure. + const raced = await this.findByIdempotencyKey(validated.idempotencyKey); + if (!raced) throw new Error(`transaction ${validated.idempotencyKey} vanished mid-post.`); + return { ...raced, isNew: false }; + } + + await tx.insert(ledgerEntries).values( + validated.entries.map((entry) => ({ + transactionId: posted.id, + economyId: entry.economyId, + organizationId: entry.organizationId, + accountId: entry.accountId, + direction: entry.direction, + amount: entry.amount, + type: entry.type, + sourceType: entry.sourceType, + sourceId: entry.sourceId, + counterpartyAccountId: entry.counterpartyAccountId, + rulesVersion: entry.rulesVersion, + idempotencyKey: entry.idempotencyKey, + status: entry.status, + settledAt: entry.settledAt === null ? null : new Date(entry.settledAt), + metadata: entry.metadata, + })), + ); + + // Law XXI, checked on every account this moved rather than only the ones + // it debited. A transaction type that credits somewhere it should not is + // still caught, and checking all of them costs one query. + for (const accountId of affected) { + // Read as a plain bigint, not as Kredbits. `kredbits()` refuses a + // negative value, which is the invariant working, and branding before + // the check would raise a RangeError instead of the overdraft this + // needs to report. The type cannot hold the thing being detected. + const balance = await this.rawBalance(tx, accountId); + if (balance < 0n) { + throw new InsufficientBalanceError(accountId, kredbits(-balance)); + } + } + + return { id: posted.id, idempotencyKey: posted.idempotencyKey, isNew: true }; + }); + } + + /** + * An account's balance, derived. + * + * Never stored. 06: "A stored balance is faster to read and vastly simpler to + * write. It is also unauditable, unreversible, and impossible to reconcile, + * and the moment it drifts from reality by one kredbit, there is no way to + * find out why." + */ + async balanceOf(accountId: string): Promise { + return kredbits(await this.rawBalance(this.db, accountId)); + } + + /** + * What has settled, which is not the same as what has been earned. + * + * Law VII, Extraction Is Not Guaranteed. A pending entry is value that + * happened and is still inside the settlement window, and treating the two as + * one number is how an economy pays out money it has not finished verifying. + */ + async settledBalanceOf(accountId: string): Promise { + const [row] = await this.db + .select({ total: this.netAmount() }) + .from(ledgerEntries) + .where(and(eq(ledgerEntries.accountId, accountId), eq(ledgerEntries.status, "SETTLED"))); + return kredbits(BigInt(row?.total ?? 0)); + } + + /** Every entry for one account, oldest first, so history explains the balance. */ + async entriesFor(accountId: string, limit = 200) { + return this.db + .select() + .from(ledgerEntries) + .where(eq(ledgerEntries.accountId, accountId)) + .orderBy(sql`${ledgerEntries.createdAt} asc`) + .limit(limit); + } + + /** The entries of one transaction, for auditing that it balanced. */ + async entriesOf(transactionId: string) { + return this.db + .select() + .from(ledgerEntries) + .where(eq(ledgerEntries.transactionId, transactionId)); + } + + private async findByIdempotencyKey( + key: string, + ): Promise<{ id: string; idempotencyKey: string } | null> { + const [row] = await this.db + .select({ id: ledgerTransactions.id, idempotencyKey: ledgerTransactions.idempotencyKey }) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.idempotencyKey, key)) + .limit(1); + return row ?? null; + } + + /** + * The unbranded sum. + * + * `Kredbits` cannot be negative by construction, so a balance that has gone + * negative cannot be expressed in it. Everything that needs to *detect* one + * reads this and brands afterwards. + */ + private async rawBalance(db: Queryable, accountId: string): Promise { + const [row] = await db + .select({ total: this.netAmount() }) + .from(ledgerEntries) + .where(eq(ledgerEntries.accountId, accountId)); + return BigInt(row?.total ?? 0); + } + + /** + * Credits minus debits, in one place. + * + * `MEMO` entries are excluded because they move no KRED: `RECEIVABLE_CREATED` + * and `RECEIVABLE_CANCELLED` record a liability appearing and disappearing, + * and counting them would make a claim look like money. + * + * Summed in the database as `numeric` and read back as a string, then widened + * to `BigInt`. A `SUM` over `bigint` that came back as a JavaScript number + * would be the floating point error 06 forbids, arriving through the one + * place nobody looks. + */ + private netAmount() { + return sql`coalesce(sum( + case ${ledgerEntries.direction} + when 'CREDIT' then ${ledgerEntries.amount} + when 'DEBIT' then -${ledgerEntries.amount} + else 0 + end + ), 0)::text`; + } +} diff --git a/packages/database/src/schema/index.ts b/packages/database/src/schema/index.ts index a8713a0..db10462 100644 --- a/packages/database/src/schema/index.ts +++ b/packages/database/src/schema/index.ts @@ -25,3 +25,21 @@ export { contributionKind, invalidationTrigger, } from "./contribution.js"; +export { + accountRelations, + accountType, + accounts, + currencies, + currencyType, + economies, + economyRelations, + economyType, + entryDirection, + entrySourceType, + entryStatus, + ledgerEntries, + ledgerEntryRelations, + ledgerTransactionRelations, + ledgerTransactions, + transactionType, +} from "./ledger.js"; diff --git a/packages/database/src/schema/ledger.ts b/packages/database/src/schema/ledger.ts new file mode 100644 index 0000000..89bef45 --- /dev/null +++ b/packages/database/src/schema/ledger.ts @@ -0,0 +1,294 @@ +import { relations } from "drizzle-orm"; +import { + bigint, + index, + integer, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; + +import { organizations } from "./github"; + +/** Mirrors `EconomyType` in `@kreds/domain`. See 14 to 16 on sovereign economies. */ +export const economyType = pgEnum("economy_type", [ + "KREDS_NETWORK", + "SOVEREIGN_NETWORK", + "INDEPENDENT", +]); + +/** Mirrors `CurrencyType`. Law X: official KRED and a local currency are not the same thing. */ +export const currencyType = pgEnum("currency_type", ["KRED", "LOCAL"]); + +/** Mirrors `AccountType`. */ +export const accountType = pgEnum("account_type", [ + "CENTRAL_BANK_RESERVE", + "GLOBAL_WALLET", + "ORGANIZATION_POSITION", + "TREASURY", + "REVIEW_FUND", + "PENDING", + "NETWORK_RESERVE", + "PROTOCOL", + "BURNED", +]); + +/** Mirrors `TransactionType`. 06: Ledger, Transaction types. */ +export const transactionType = pgEnum("transaction_type", [ + "DISTRIBUTION", + "TRANSFER", + "FEE", + "REFUND", + "REVERSAL", + "TREASURY_CONTRIBUTION", + "TREASURY_DISTRIBUTION", + "BURN", + "ADJUSTMENT", + "RESERVE_ALLOCATION", + "EXCHANGE", + "SETTLEMENT", + "REVIEW_FUND_CONTRIBUTION", + "REVIEW_FUND_PAYMENT", + "CREDIT_DRAW", + "DEBT_REPAYMENT", + "RECEIVABLE_CREATED", + "RECEIVABLE_SETTLED", + "RECEIVABLE_CANCELLED", +]); + +/** + * Mirrors `EntryDirection`. + * + * `MEMO` exists for the two entry types that move no KRED at all, + * `RECEIVABLE_CREATED` and `RECEIVABLE_CANCELLED`. 06 keeps them in the ledger + * rather than in a side table on purpose: "a claim that is invisible to the + * ledger is a claim nobody can audit." + */ +export const entryDirection = pgEnum("entry_direction", ["DEBIT", "CREDIT", "MEMO"]); + +/** Mirrors `EntrySourceType`. Traces every entry back to what caused it. */ +export const entrySourceType = pgEnum("entry_source_type", [ + "PULL_REQUEST_MERGED", + "PULL_REQUEST_CLOSED", + "REVIEW_SUBMITTED", + "SETTLEMENT_RUN", + "TREASURY_OPERATION", + "CREDIT_OPERATION", + "NETWORK_OPERATION", + "MANUAL_ADJUSTMENT", +]); + +/** Mirrors `EntryStatus`. Law VII: earned is not the same as withdrawable. */ +export const entryStatus = pgEnum("entry_status", ["PENDING", "SETTLED"]); + +/** + * An economy: the boundary a currency and its accounts live inside. + * + * Law IV makes a GitHub organization the default economic boundary, and Law X + * keeps official KRED separate from local currencies. Both are enforced by + * every account and entry naming the economy it belongs to. + */ +export const economies = pgTable( + "economies", + { + id: uuid("id").primaryKey().defaultRandom(), + type: economyType("type").notNull(), + /** Null for a network-level economy that belongs to no single organization. */ + organizationId: uuid("organization_id").references(() => organizations.id, { + onDelete: "set null", + }), + name: text("name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("economies_organization_idx").on(table.organizationId)], +); + +/** + * A currency. + * + * `subunitsPerUnit` is stored rather than assumed. 06 says the subunit name may + * change but the integer requirement may not, and a local economy is free to + * choose its own scale. Nothing reads a hard-coded 100. + */ +export const currencies = pgTable( + "currencies", + { + id: uuid("id").primaryKey().defaultRandom(), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + type: currencyType("type").notNull(), + /** `KRED`, `ZIT`. Display only. */ + code: text("code").notNull(), + name: text("name").notNull(), + subunitsPerUnit: integer("subunits_per_unit").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("currencies_economy_idx").on(table.economyId)], +); + +/** + * An account. + * + * **There is deliberately no balance column.** + * + * 06: Ledger is blunt about why, and it is worth keeping the reason next to the + * absence: "A stored balance is faster to read and vastly simpler to write. It + * is also unauditable, unreversible, and impossible to reconcile, and the + * moment it drifts from reality by one kredbit, there is no way to find out + * why." + * + * Balances are derived from entries. Every time. The absence of the column is + * what makes that true rather than aspirational. + */ +export const accounts = pgTable( + "accounts", + { + id: uuid("id").primaryKey().defaultRandom(), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + type: accountType("type").notNull(), + /** + * The GitHub identity this account belongs to, when it belongs to a person. + * + * Keyed on the identity rather than the Kreds account, for the same reason + * contributions are: Law XVII lets an identity earn before it has an + * account, so keying on the account would leave that value belonging to + * nobody. + */ + ownerGitHubUserId: bigint("owner_github_user_id", { mode: "number" }), + /** Enforces the organization boundary (Law IV). Null for network-level accounts. */ + organizationId: uuid("organization_id").references(() => organizations.id, { + onDelete: "set null", + }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + index("accounts_economy_owner_idx").on(table.economyId, table.ownerGitHubUserId), + index("accounts_economy_type_idx").on(table.economyId, table.type), + index("accounts_organization_idx").on(table.organizationId), + ], +); + +/** + * A grouped, balanced set of entries. + * + * Nothing reaches the ledger except through one of these, which is what makes + * the balance invariant checkable: entries are written together or not at all, + * and a transaction whose entries do not sum to zero is rejected before any of + * it lands. + */ +export const ledgerTransactions = pgTable( + "ledger_transactions", + { + id: uuid("id").primaryKey().defaultRandom(), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + type: transactionType("type").notNull(), + /** + * Makes a duplicate webhook harmless. + * + * 06: "GitHub explicitly delivers webhooks *at least once*: retries after + * timeouts, redeliveries, and duplicate events under load are normal + * operation, not failure." + */ + idempotencyKey: text("idempotency_key").notNull().unique(), + /** Law XV: makes history immune to policy changes. */ + rulesVersion: text("rules_version").notNull(), + metadata: jsonb("metadata"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("ledger_transactions_economy_idx").on(table.economyId, table.createdAt)], +); + +/** + * One side of a movement. + * + * `amount` is a non-negative integer count of subunits and `direction` carries + * the sign. Two columns rather than one signed number, because a query that + * forgot the sign would silently compute the wrong balance, and a reader + * scanning rows can see what each one did. + * + * `bigint` in `bigint` mode, so values round-trip as BigInt and never touch a + * JavaScript number. 06: "Floating point silently loses value. `0.1 + 0.2 !== + * 0.3` is a curiosity in a tutorial and a supply-conservation violation here." + */ +export const ledgerEntries = pgTable( + "ledger_entries", + { + id: uuid("id").primaryKey().defaultRandom(), + transactionId: uuid("transaction_id") + .notNull() + .references(() => ledgerTransactions.id, { onDelete: "restrict" }), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + organizationId: uuid("organization_id").references(() => organizations.id, { + onDelete: "set null", + }), + accountId: uuid("account_id") + .notNull() + .references(() => accounts.id, { onDelete: "restrict" }), + direction: entryDirection("direction").notNull(), + /** Non-negative subunits. The sign lives in `direction`. */ + amount: bigint("amount", { mode: "bigint" }).notNull(), + type: transactionType("type").notNull(), + sourceType: entrySourceType("source_type").notNull(), + /** What caused this, traceable back to the GitHub event. */ + sourceId: text("source_id").notNull(), + /** Makes every transfer two-sided and reconcilable. */ + counterpartyAccountId: uuid("counterparty_account_id").references(() => accounts.id, { + onDelete: "set null", + }), + rulesVersion: text("rules_version").notNull(), + idempotencyKey: text("idempotency_key").notNull(), + status: entryStatus("status").notNull().default("PENDING"), + /** Law VII: separates earned from withdrawable. */ + settledAt: timestamp("settled_at", { withTimezone: true }), + metadata: jsonb("metadata"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + // Deriving a balance: every entry for one account. + index("ledger_entries_account_idx").on(table.accountId, table.status), + index("ledger_entries_transaction_idx").on(table.transactionId), + index("ledger_entries_economy_idx").on(table.economyId, table.createdAt), + // Tracing an entry back to what caused it. + index("ledger_entries_source_idx").on(table.sourceType, table.sourceId), + ], +); + +export const economyRelations = relations(economies, ({ many, one }) => ({ + currencies: many(currencies), + accounts: many(accounts), + organization: one(organizations, { + fields: [economies.organizationId], + references: [organizations.id], + }), +})); + +export const accountRelations = relations(accounts, ({ one, many }) => ({ + economy: one(economies, { fields: [accounts.economyId], references: [economies.id] }), + entries: many(ledgerEntries), +})); + +export const ledgerTransactionRelations = relations(ledgerTransactions, ({ many, one }) => ({ + entries: many(ledgerEntries), + economy: one(economies, { + fields: [ledgerTransactions.economyId], + references: [economies.id], + }), +})); + +export const ledgerEntryRelations = relations(ledgerEntries, ({ one }) => ({ + transaction: one(ledgerTransactions, { + fields: [ledgerEntries.transactionId], + references: [ledgerTransactions.id], + }), + account: one(accounts, { fields: [ledgerEntries.accountId], references: [accounts.id] }), +}));