@vrplatform/kysely 1.3.45-stage.11 → 1.3.45-stage.1104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/build/.data.tar +2 -2
  2. package/build/main/control-plane/index.d.ts +28 -6
  3. package/build/main/control-plane/index.js.map +1 -1
  4. package/build/main/index.d.ts +23 -1
  5. package/build/main/index.js +209 -50
  6. package/build/main/index.js.map +1 -1
  7. package/build/main/local/index.js +115 -8
  8. package/build/main/local/index.js.map +1 -1
  9. package/build/main/postgres-error.d.ts +3 -3
  10. package/build/main/postgres-error.js +4 -3
  11. package/build/main/postgres-error.js.map +1 -1
  12. package/build/main/query/tenant/status.d.ts +1 -1
  13. package/build/main/query/tenant/status.js +5 -2
  14. package/build/main/query/tenant/status.js.map +1 -1
  15. package/build/main/regional.d.ts +1 -0
  16. package/build/main/regional.js +1 -0
  17. package/build/main/regional.js.map +1 -1
  18. package/build/main/test/index.d.ts +25 -2
  19. package/build/main/test/index.js +40 -8
  20. package/build/main/test/index.js.map +1 -1
  21. package/build/main/v1.generated.d.ts +193 -19
  22. package/build/main/v1.generated.js.map +1 -1
  23. package/build/migrations-v2/0000000000031-ui-permission-role-simplification.ts +70 -0
  24. package/build/migrations-v2/0000000000032-tenant-delete-ownership-cascades.ts +44 -0
  25. package/build/migrations-v2/0000000000033-plaid-connect-session-telemetry.ts +31 -0
  26. package/build/migrations-v2/0000000000033-reservation-issue-seed-bucket-index.ts +19 -0
  27. package/build/migrations-v2/0000000000034-retire-recent-classification-indexes.ts +29 -0
  28. package/build/migrations-v2/0000000000035-core-change-autovacuum.ts +24 -0
  29. package/build/migrations-v2/0000000000036-journal-entry-inactive-reason-constraint.ts +33 -0
  30. package/build/migrations-v2/0000000000037-journal-entry-inactive-reasons.ts +19 -0
  31. package/build/migrations-v2/0000000000038-journal-entry-group-inactive-reason.ts +14 -0
  32. package/build/migrations-v2/0000000000039-reservation-guest-breakdown.ts +19 -0
  33. package/build/migrations-v2/0000000000040-csv-import-source-identity-index.ts +23 -0
  34. package/build/migrations-v2/0000000000041-payment-returns.ts +38 -0
  35. package/build/migrations-v2/0000000000042-operation-resource-types.ts +609 -0
  36. package/build/migrations-v2/0000000000043-reservation-journal-freshness.ts +149 -0
  37. package/build/migrations-v2/0000000000044-reservation-journal-freshness-backfill.ts +254 -0
  38. package/build/migrations-v2/0000000000045-reservation-journal-freshness-revision-repair.ts +83 -0
  39. package/build/migrations-v2/0000000000046-materialized-reservation-journal-operations.ts +709 -0
  40. package/build/migrations-v2/0000000000047-public-audit-completion.ts +77 -0
  41. package/build/migrations-v2/0000000000048-reservation-journal-full-refresh-repair.ts +136 -0
  42. package/build/migrations-v2/0000000000049-queued-journal-operation-failures.ts +60 -0
  43. package/build/migrations-v2/0000000000050-connection-oauth-state.ts +43 -0
  44. package/build/migrations-v2/0000000000051-owner-statement-email-delivery.ts +134 -0
  45. package/build/migrations-v2/0000000000052-fee-journal-lock-skip-operations.ts +59 -0
  46. package/build/migrations-v2/0000000000053-fee-journal-lock-skip-null-attempt.ts +44 -0
  47. package/build/migrations-v2/0000000000054-journal-refresh-issue-codes.ts +16 -0
  48. package/build/migrations-v2/0000000000055-tenant-billing-subscription.ts +8 -0
  49. package/build/migrations-v2/0000000000056-journal-operation-failure-precedence.ts +306 -0
  50. package/build/migrations-v2/0000000000057-journal-invalidation-failure-code.ts +28 -0
  51. package/build/migrations-v2/0000000000058-journal-invalidation-locked-failure.ts +16 -0
  52. package/build/migrations-v2/0000000000059-billing-active-listing-cross-region-partner.ts +135 -0
  53. package/build/migrations-v2/0000000000060-accounting-integrity-change-driven.ts +99 -0
  54. package/build/migrations-v2/0000000000061-tenant-pending-onboarding-status.ts +37 -0
  55. package/build/migrations-v2/0000000000062-statement-payout-operations.ts +16 -0
  56. package/build/migrations-v2/0000000000063-missing-account-assignments-issue-code.ts +13 -0
  57. package/build/module/control-plane/index.d.ts +28 -6
  58. package/build/module/control-plane/index.js.map +1 -1
  59. package/build/module/index.d.ts +23 -1
  60. package/build/module/index.js +206 -50
  61. package/build/module/index.js.map +1 -1
  62. package/build/module/local/index.js +115 -8
  63. package/build/module/local/index.js.map +1 -1
  64. package/build/module/postgres-error.d.ts +3 -3
  65. package/build/module/postgres-error.js +4 -3
  66. package/build/module/postgres-error.js.map +1 -1
  67. package/build/module/query/tenant/status.d.ts +1 -1
  68. package/build/module/query/tenant/status.js +5 -2
  69. package/build/module/query/tenant/status.js.map +1 -1
  70. package/build/module/regional.d.ts +1 -0
  71. package/build/module/regional.js +1 -0
  72. package/build/module/regional.js.map +1 -1
  73. package/build/module/test/index.d.ts +25 -2
  74. package/build/module/test/index.js +39 -8
  75. package/build/module/test/index.js.map +1 -1
  76. package/build/module/v1.generated.d.ts +193 -19
  77. package/build/module/v1.generated.js.map +1 -1
  78. package/package.json +9 -7
@@ -0,0 +1,306 @@
1
+ import { type Kysely, sql } from 'kysely';
2
+
3
+ // The served `core.operation.failure_code` projection must agree with the
4
+ // TypeScript classifier in `packages/repository/src/journal-freshness/load.ts`:
5
+ // a governing accounting lock wins over a retained failure (CUS-2936), and a
6
+ // retained unbalanced attempt is recognized by its message prefix as well as by
7
+ // the typed result code (CUS-2894).
8
+ const failureProjection = (ordering: 'lock-first' | 'failure-first') => sql`
9
+ create or replace function core.refresh_operation_state(
10
+ target_operation_id uuid
11
+ ) returns void
12
+ language plpgsql
13
+ as $$
14
+ declare
15
+ next_status core.operation_status;
16
+ next_started_at timestamptz;
17
+ next_finished_at timestamptz;
18
+ next_failure_code text;
19
+ next_failure_message text;
20
+ target_operation_type core.operation_type;
21
+ target_public_id_format core.operation_public_id_format;
22
+ target_action_id uuid;
23
+ begin
24
+ select
25
+ operation.type,
26
+ operation.public_id_format,
27
+ source.source_id
28
+ into
29
+ target_operation_type,
30
+ target_public_id_format,
31
+ target_action_id
32
+ from core.operation as operation
33
+ left join core.operation_source as source
34
+ on source.operation_id = operation.id
35
+ and source.source_type = 'audit-action'
36
+ where operation.id = target_operation_id;
37
+
38
+ select
39
+ case
40
+ when count(*) filter (where required) = 0 then 'queued'
41
+ when bool_and(status = 'completed') filter (where required)
42
+ then 'completed'
43
+ when bool_and(status in ('completed', 'failed')) filter (where required)
44
+ then 'failed'
45
+ when bool_or(status = 'running') filter (where required)
46
+ or bool_or(status in ('completed', 'failed')) filter (where required)
47
+ then 'running'
48
+ else 'queued'
49
+ end::core.operation_status,
50
+ min(started_at) filter (where required),
51
+ max(finished_at) filter (where required)
52
+ into next_status, next_started_at, next_finished_at
53
+ from core.operation_source
54
+ where operation_id = target_operation_id;
55
+
56
+ if next_status = 'failed' then
57
+ if target_operation_type = 'reservation-journal-refresh'
58
+ and target_action_id is not null then
59
+ select
60
+ case
61
+ ${sql.raw(
62
+ ordering === 'lock-first'
63
+ ? `when core.journal_refresh_all_outstanding_locked(
64
+ bool_and(
65
+ case
66
+ when invalidation.id is not null
67
+ and invalidation.applied_at is null
68
+ then effect.status = 'completed'
69
+ and attempt.status is not distinct from 'skipped_locked'
70
+ end
71
+ ),
72
+ bool_and(
73
+ case
74
+ when invalidation.id is not null
75
+ and invalidation.applied_at is null
76
+ then core.journal_reservation_refresh_locked(
77
+ invalidation.tenant_id,
78
+ invalidation.reservation_id
79
+ )
80
+ end
81
+ )
82
+ ) then 'journalLocked'
83
+ when bool_or(
84
+ invalidation.applied_at is null
85
+ and attempt.result_code = 'journal_configuration_invalid'
86
+ ) then 'journalConfigurationInvalid'
87
+ when bool_or(
88
+ invalidation.applied_at is null
89
+ and (
90
+ attempt.result_code = 'journal_unbalanced'
91
+ or attempt.error_message like 'Journal refresh produced unbalanced%'
92
+ )
93
+ ) then 'journalUnbalanced'`
94
+ : `when bool_or(
95
+ invalidation.applied_at is null
96
+ and attempt.result_code = 'journal_unbalanced'
97
+ ) then 'journalUnbalanced'
98
+ when bool_or(
99
+ invalidation.applied_at is null
100
+ and attempt.result_code = 'journal_configuration_invalid'
101
+ ) then 'journalConfigurationInvalid'
102
+ when bool_or(
103
+ invalidation.applied_at is null
104
+ and attempt.result_code in (
105
+ 'statement_period_locked',
106
+ 'skipped_locked'
107
+ )
108
+ ) then 'journalLocked'`
109
+ )}
110
+ else 'journalRefreshFailed'
111
+ end,
112
+ case
113
+ ${sql.raw(
114
+ ordering === 'lock-first'
115
+ ? `when core.journal_refresh_all_outstanding_locked(
116
+ bool_and(
117
+ case
118
+ when invalidation.id is not null
119
+ and invalidation.applied_at is null
120
+ then effect.status = 'completed'
121
+ and attempt.status is not distinct from 'skipped_locked'
122
+ end
123
+ ),
124
+ bool_and(
125
+ case
126
+ when invalidation.id is not null
127
+ and invalidation.applied_at is null
128
+ then core.journal_reservation_refresh_locked(
129
+ invalidation.tenant_id,
130
+ invalidation.reservation_id
131
+ )
132
+ end
133
+ )
134
+ ) then 'Journal refresh is blocked by accounting locks'
135
+ when bool_or(
136
+ invalidation.applied_at is null
137
+ and attempt.result_code = 'journal_configuration_invalid'
138
+ ) then 'Journal configuration is invalid'
139
+ when bool_or(
140
+ invalidation.applied_at is null
141
+ and (
142
+ attempt.result_code = 'journal_unbalanced'
143
+ or attempt.error_message like 'Journal refresh produced unbalanced%'
144
+ )
145
+ ) then 'Journal refresh produced unbalanced entries'`
146
+ : `when bool_or(
147
+ invalidation.applied_at is null
148
+ and attempt.result_code = 'journal_unbalanced'
149
+ ) then 'Journal refresh produced unbalanced entries'
150
+ when bool_or(
151
+ invalidation.applied_at is null
152
+ and attempt.result_code = 'journal_configuration_invalid'
153
+ ) then 'Journal configuration is invalid'
154
+ when bool_or(
155
+ invalidation.applied_at is null
156
+ and attempt.result_code in (
157
+ 'statement_period_locked',
158
+ 'skipped_locked'
159
+ )
160
+ ) then 'Journal refresh is blocked by accounting locks'`
161
+ )}
162
+ else 'Journal refresh failed'
163
+ end
164
+ into next_failure_code, next_failure_message
165
+ from audit.effect as effect
166
+ left join accounting.reservation_journal_invalidation as invalidation
167
+ on invalidation.tenant_id = effect.tenant_id
168
+ and invalidation.effect_id = effect.id
169
+ left join lateral (
170
+ select
171
+ effect_attempt.status,
172
+ effect_attempt.result_code,
173
+ effect_attempt.error_message
174
+ from audit.effect_attempt as effect_attempt
175
+ where effect_attempt.tenant_id = effect.tenant_id
176
+ and effect_attempt.effect_id = effect.id
177
+ order by effect_attempt.attempt_num desc
178
+ limit 1
179
+ ) as attempt on true
180
+ where effect.action_id = target_action_id
181
+ and effect.effect_type = 'REFRESH_RESERVATION_JOURNAL'
182
+ and effect.entity_type = 'reservation';
183
+ else
184
+ next_failure_code := 'operation_failed';
185
+ next_failure_message := 'Operation failed';
186
+ end if;
187
+ else
188
+ next_failure_code := null;
189
+ next_failure_message := null;
190
+ end if;
191
+
192
+ update core.operation
193
+ set
194
+ status = next_status,
195
+ started_at = coalesce(core.operation.started_at, next_started_at),
196
+ finished_at = case
197
+ when next_status in ('completed', 'failed')
198
+ then coalesce(next_finished_at, now())
199
+ else null
200
+ end,
201
+ failure_code = next_failure_code,
202
+ failure_message = next_failure_message,
203
+ updated_at = now(),
204
+ source_version = source_version + 1
205
+ where id = target_operation_id
206
+ and (
207
+ status is distinct from next_status
208
+ or started_at is distinct from coalesce(started_at, next_started_at)
209
+ or finished_at is distinct from case
210
+ when next_status in ('completed', 'failed')
211
+ then coalesce(next_finished_at, finished_at, now())
212
+ else null
213
+ end
214
+ or failure_code is distinct from next_failure_code
215
+ or failure_message is distinct from next_failure_message
216
+ );
217
+ end;
218
+ $$
219
+ `;
220
+
221
+ // The reservation lock driver from `isReservationRefreshLocked`: the refresh is
222
+ // locked when every active reservation entry sits in a published statement or
223
+ // in closed books.
224
+ const reservationRefreshLocked = sql`
225
+ create or replace function core.journal_reservation_refresh_locked(
226
+ target_tenant_id uuid,
227
+ target_reservation_id uuid
228
+ ) returns boolean
229
+ language sql
230
+ stable
231
+ as $$
232
+ select exists (
233
+ select 1
234
+ from accounting.journal_entry as entry
235
+ inner join public.tenant as tenant on tenant.id = entry.tenant_id
236
+ where entry.tenant_id = target_tenant_id
237
+ and entry.reservation_id = target_reservation_id
238
+ and entry.status = 'active'
239
+ and (entry.entity_type is null or entry.entity_type = 'reservation')
240
+ and (
241
+ entry.attached_to_owner_statement_id is not null
242
+ or entry.transfer_owner_statement_id is not null
243
+ or entry.txn_at < tenant.books_closed_at
244
+ )
245
+ )
246
+ $$
247
+ `;
248
+
249
+ // `bool_and` over no outstanding invalidation returns null; the classifier
250
+ // treats that as "not locked".
251
+ const allOutstandingLocked = sql`
252
+ create or replace function core.journal_refresh_all_outstanding_locked(
253
+ all_lock_skipped boolean,
254
+ all_reservations_locked boolean
255
+ ) returns boolean
256
+ language sql
257
+ immutable
258
+ as $$
259
+ select coalesce(all_lock_skipped, false) or coalesce(all_reservations_locked, false)
260
+ $$
261
+ `;
262
+
263
+ async function recomputeFailedJournalOperations(db: Kysely<unknown>) {
264
+ // Derived operation state only. Attempts, invalidations and journals are untouched.
265
+ await sql`
266
+ select core.refresh_operation_state(operation.id)
267
+ from core.operation as operation
268
+ where operation.type = 'reservation-journal-refresh'
269
+ and operation.status = 'failed'
270
+ and exists (
271
+ select 1 from core.operation_source as source
272
+ where source.operation_id = operation.id
273
+ and source.source_type = 'audit-action'
274
+ )
275
+ `.execute(db);
276
+ }
277
+
278
+ export async function up(db: Kysely<unknown>): Promise<void> {
279
+ await sql`
280
+ do $$
281
+ begin
282
+ if position(
283
+ 'and target_public_id_format = ''action'''
284
+ in pg_get_functiondef('core.refresh_operation_state(uuid)'::regprocedure)
285
+ ) > 0 then
286
+ raise exception 'Journal operation failure projection has an unexpected definition';
287
+ end if;
288
+ end;
289
+ $$
290
+ `.execute(db);
291
+ await reservationRefreshLocked.execute(db);
292
+ await allOutstandingLocked.execute(db);
293
+ await failureProjection('lock-first').execute(db);
294
+ await recomputeFailedJournalOperations(db);
295
+ }
296
+
297
+ export async function down(db: Kysely<unknown>): Promise<void> {
298
+ await failureProjection('failure-first').execute(db);
299
+ await recomputeFailedJournalOperations(db);
300
+ await sql`drop function if exists core.journal_refresh_all_outstanding_locked(boolean, boolean)`.execute(
301
+ db
302
+ );
303
+ await sql`drop function if exists core.journal_reservation_refresh_locked(uuid, uuid)`.execute(
304
+ db
305
+ );
306
+ }
@@ -0,0 +1,28 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ // An inline reservation journal refresh has no effect row, so a refresh that
5
+ // completes without writing the reservation's postings had nowhere to record
6
+ // why. Storage only: nullable column, no default, no backfill, no rewrite.
7
+ export async function up(db: Kysely<unknown>): Promise<void> {
8
+ await sql`
9
+ create type accounting.reservation_journal_invalidation_failure as enum (
10
+ 'journal_configuration_invalid'
11
+ )
12
+ `.execute(db);
13
+ await sql`
14
+ alter table accounting.reservation_journal_invalidation
15
+ add column failure_code
16
+ accounting.reservation_journal_invalidation_failure
17
+ `.execute(db);
18
+ }
19
+
20
+ export async function down(db: Kysely<unknown>): Promise<void> {
21
+ await sql`
22
+ alter table accounting.reservation_journal_invalidation
23
+ drop column if exists failure_code
24
+ `.execute(db);
25
+ await sql`
26
+ drop type if exists accounting.reservation_journal_invalidation_failure
27
+ `.execute(db);
28
+ }
@@ -0,0 +1,16 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ // An inline reservation journal refresh refused by an accounting lock records
5
+ // why on its invalidation, like a discarded build does. Storage only: the
6
+ // value must be readable on every channel before the writer starts using it.
7
+ export async function up(db: Kysely<unknown>): Promise<void> {
8
+ await sql`
9
+ alter type accounting.reservation_journal_invalidation_failure
10
+ add value if not exists 'journal_locked'
11
+ `.execute(db);
12
+ }
13
+
14
+ export async function down(_db: Kysely<unknown>): Promise<void> {
15
+ // PostgreSQL enum values cannot be removed safely without rebuilding the type.
16
+ }
@@ -0,0 +1,135 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ // CUS-2977: a billing partner homed in another data region has no row in this
5
+ // regional database. That absence is valid and must not make the team count
6
+ // zero active listings. Only a locally present, non-active billing partner
7
+ // does, matching kysely `whereTenantActive` (PRO-17712). `create or replace`
8
+ // keeps the existing grants.
9
+ export async function up(db: Kysely<unknown>): Promise<void> {
10
+ await sql`
11
+ create or replace function billing.active_listing_count(
12
+ selected_tenant_id uuid
13
+ ) returns integer
14
+ language sql
15
+ stable
16
+ as $$
17
+ select count(*)::integer
18
+ from public.listing as listing
19
+ inner join public.tenant as tenant
20
+ on tenant.id = listing.tenant_id
21
+ where listing.tenant_id = selected_tenant_id
22
+ and tenant.calculated_status = 'active'
23
+ and not exists (
24
+ select 1
25
+ from public.tenant as billing_partner
26
+ where billing_partner.id = coalesce(
27
+ tenant.calculated_billing_partner_id,
28
+ '7bfec41f-aa3f-4602-95d2-f7996e4f0e59'::uuid
29
+ )
30
+ and billing_partner.calculated_status is distinct from 'active'
31
+ )
32
+ and (
33
+ (
34
+ tenant.is_general_ledger = false
35
+ and listing.calculated_status = 'active'
36
+ ) or (
37
+ tenant.is_general_ledger = true
38
+ and not exists (
39
+ select 1
40
+ from public.listing_ownership_period as ownership
41
+ where ownership.listing_id = coalesce(
42
+ listing.parent_id,
43
+ listing.id
44
+ )
45
+ and ownership.tenant_id = listing.tenant_id
46
+ and ownership.set_listing_inactive = true
47
+ and ownership.start_at <= current_date
48
+ and (
49
+ ownership.end_at is null
50
+ or ownership.end_at >= current_date
51
+ )
52
+ )
53
+ )
54
+ )
55
+ $$
56
+ `.execute(db);
57
+
58
+ // Append a corrected fact, effective now, for every active team whose
59
+ // billing partner has no local row. These are the only teams whose count
60
+ // changes. Completed-month billing reads the latest fact before the month
61
+ // ends, so applying this before the month closes corrects that month.
62
+ // Earlier wrong facts stay as recorded history.
63
+ await sql`
64
+ select billing.record_listing_facts(
65
+ array(
66
+ select tenant.id
67
+ from public.tenant as tenant
68
+ where tenant.type = 'propertyManager'
69
+ and tenant.calculated_status = 'active'
70
+ and tenant.calculated_billing_partner_id is not null
71
+ and not exists (
72
+ select 1
73
+ from public.tenant as billing_partner
74
+ where billing_partner.id = tenant.calculated_billing_partner_id
75
+ )
76
+ and exists (
77
+ select 1
78
+ from billing.listing_fact as fact
79
+ where fact.tenant_id = tenant.id
80
+ )
81
+ ),
82
+ 'daily_reconciliation',
83
+ gen_random_uuid(),
84
+ now(),
85
+ true
86
+ )
87
+ `.execute(db);
88
+ }
89
+
90
+ export async function down(db: Kysely<unknown>): Promise<void> {
91
+ await sql`
92
+ create or replace function billing.active_listing_count(
93
+ selected_tenant_id uuid
94
+ ) returns integer
95
+ language sql
96
+ stable
97
+ as $$
98
+ select count(*)::integer
99
+ from public.listing as listing
100
+ inner join public.tenant as tenant
101
+ on tenant.id = listing.tenant_id
102
+ left join public.tenant as billing_partner
103
+ on billing_partner.id = tenant.calculated_billing_partner_id
104
+ where listing.tenant_id = selected_tenant_id
105
+ and tenant.calculated_status = 'active'
106
+ and (
107
+ tenant.calculated_billing_partner_id is null
108
+ or billing_partner.calculated_status = 'active'
109
+ )
110
+ and (
111
+ (
112
+ tenant.is_general_ledger = false
113
+ and listing.calculated_status = 'active'
114
+ ) or (
115
+ tenant.is_general_ledger = true
116
+ and not exists (
117
+ select 1
118
+ from public.listing_ownership_period as ownership
119
+ where ownership.listing_id = coalesce(
120
+ listing.parent_id,
121
+ listing.id
122
+ )
123
+ and ownership.tenant_id = listing.tenant_id
124
+ and ownership.set_listing_inactive = true
125
+ and ownership.start_at <= current_date
126
+ and (
127
+ ownership.end_at is null
128
+ or ownership.end_at >= current_date
129
+ )
130
+ )
131
+ )
132
+ )
133
+ $$
134
+ `.execute(db);
135
+ }
@@ -0,0 +1,99 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ export async function up(db: Kysely<unknown>): Promise<void> {
5
+ await sql`
6
+ create type health.accounting_integrity_dirty_record_type as enum (
7
+ 'reservation',
8
+ 'transaction'
9
+ )
10
+ `.execute(db);
11
+
12
+ await sql`
13
+ create table health.accounting_integrity_dirty_record (
14
+ tenant_id uuid not null references public.tenant(id) on delete cascade,
15
+ record_type health.accounting_integrity_dirty_record_type not null,
16
+ record_id uuid not null,
17
+ dirty_version bigint not null default 1,
18
+ computed_version bigint not null default 0,
19
+ dirty_at timestamptz not null default now(),
20
+ computed_at timestamptz,
21
+ claimed_until timestamptz,
22
+ claim_token uuid,
23
+ created_at timestamptz not null default now(),
24
+ updated_at timestamptz not null default now(),
25
+ constraint accounting_integrity_dirty_record_identity_key
26
+ primary key (tenant_id, record_type, record_id),
27
+ constraint accounting_integrity_dirty_record_version_check
28
+ check (dirty_version >= 0 and computed_version >= 0)
29
+ )
30
+ `.execute(db);
31
+
32
+ await sql`
33
+ create index accounting_integrity_dirty_record_pending_idx
34
+ on health.accounting_integrity_dirty_record (
35
+ dirty_at,
36
+ tenant_id,
37
+ record_type,
38
+ record_id,
39
+ claimed_until
40
+ )
41
+ where dirty_version > computed_version
42
+ `.execute(db);
43
+
44
+ await sql`
45
+ create table health.accounting_integrity_sweep_cursor (
46
+ record_type health.accounting_integrity_dirty_record_type primary key,
47
+ last_tenant_id uuid,
48
+ last_record_id uuid,
49
+ cycle_started_at timestamptz not null default now(),
50
+ last_cycle_completed_at timestamptz,
51
+ definitions_hash text,
52
+ created_at timestamptz not null default now(),
53
+ updated_at timestamptz not null default now()
54
+ )
55
+ `.execute(db);
56
+
57
+ await sql`
58
+ create table health.accounting_integrity_comparison_finding (
59
+ id uuid primary key default gen_random_uuid(),
60
+ data_region text not null,
61
+ code text not null,
62
+ tenant_id uuid not null references public.tenant(id) on delete cascade,
63
+ record_type health.accounting_integrity_dirty_record_type not null,
64
+ record_id uuid not null,
65
+ case_key text not null,
66
+ severity health.accounting_integrity_severity not null,
67
+ content_hash text not null,
68
+ first_seen_at timestamptz not null,
69
+ created_at timestamptz not null default now(),
70
+ updated_at timestamptz not null default now(),
71
+ constraint accounting_integrity_comparison_finding_identity_key
72
+ unique (data_region, code, tenant_id, case_key)
73
+ )
74
+ `.execute(db);
75
+
76
+ await sql`
77
+ create index accounting_integrity_comparison_finding_report_idx
78
+ on health.accounting_integrity_comparison_finding (
79
+ data_region,
80
+ code,
81
+ case_key
82
+ )
83
+ `.execute(db);
84
+ }
85
+
86
+ export async function down(db: Kysely<unknown>): Promise<void> {
87
+ await sql`
88
+ drop table if exists health.accounting_integrity_comparison_finding
89
+ `.execute(db);
90
+ await sql`
91
+ drop table if exists health.accounting_integrity_sweep_cursor
92
+ `.execute(db);
93
+ await sql`
94
+ drop table if exists health.accounting_integrity_dirty_record
95
+ `.execute(db);
96
+ await sql`
97
+ drop type if exists health.accounting_integrity_dirty_record_type
98
+ `.execute(db);
99
+ }
@@ -0,0 +1,37 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ export async function up(db: Kysely<unknown>): Promise<void> {
5
+ await sql`set local lock_timeout = '5s'`.execute(db);
6
+ await sql`set local statement_timeout = '60s'`.execute(db);
7
+ await sql`
8
+ alter table public.tenant
9
+ alter column calculated_status set expression as (
10
+ case
11
+ when status = 'active' and (
12
+ billing_subscription_status = 'active'
13
+ or billing_subscription_status is null
14
+ or (billing_subscription_status = 'pending' and is_onboarding = true)
15
+ ) then 'active'
16
+ else 'inactive'
17
+ end
18
+ )
19
+ `.execute(db);
20
+ }
21
+
22
+ export async function down(db: Kysely<unknown>): Promise<void> {
23
+ await sql`set local lock_timeout = '5s'`.execute(db);
24
+ await sql`set local statement_timeout = '60s'`.execute(db);
25
+ await sql`
26
+ alter table public.tenant
27
+ alter column calculated_status set expression as (
28
+ case
29
+ when status = 'active' and (
30
+ billing_subscription_status = 'active'
31
+ or billing_subscription_status is null
32
+ ) then 'active'
33
+ else 'inactive'
34
+ end
35
+ )
36
+ `.execute(db);
37
+ }
@@ -0,0 +1,16 @@
1
+ import { type Kysely, sql } from 'kysely';
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await sql`alter table core.async_operation_result add column checkpoint jsonb`.execute(
5
+ db
6
+ );
7
+ await sql`alter type core.operation_type add value if not exists 'statement-payout'`.execute(
8
+ db
9
+ );
10
+ await sql`alter type core.operation_type add value if not exists 'statement-payout-preview'`.execute(
11
+ db
12
+ );
13
+ }
14
+
15
+ // PostgreSQL enum additions are retained so historical operations remain readable.
16
+ export async function down(_db: Kysely<unknown>): Promise<void> {}
@@ -0,0 +1,13 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ export async function up(db: Kysely<unknown>): Promise<void> {
5
+ // Expand storage only. The code must be writable on every channel before
6
+ // API producers mark it dirty in shared regional databases.
7
+ await sql`alter type health.tenant_issue_code add value if not exists 'missingAccountAssignments'`.execute(
8
+ db
9
+ );
10
+ }
11
+
12
+ // PostgreSQL enum additions are retained so historical issues remain readable.
13
+ export async function down(_db: Kysely<unknown>): Promise<void> {}