@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.
- package/build/.data.tar +2 -2
- package/build/main/control-plane/index.d.ts +28 -6
- package/build/main/control-plane/index.js.map +1 -1
- package/build/main/index.d.ts +23 -1
- package/build/main/index.js +209 -50
- package/build/main/index.js.map +1 -1
- package/build/main/local/index.js +115 -8
- package/build/main/local/index.js.map +1 -1
- package/build/main/postgres-error.d.ts +3 -3
- package/build/main/postgres-error.js +4 -3
- package/build/main/postgres-error.js.map +1 -1
- package/build/main/query/tenant/status.d.ts +1 -1
- package/build/main/query/tenant/status.js +5 -2
- package/build/main/query/tenant/status.js.map +1 -1
- package/build/main/regional.d.ts +1 -0
- package/build/main/regional.js +1 -0
- package/build/main/regional.js.map +1 -1
- package/build/main/test/index.d.ts +25 -2
- package/build/main/test/index.js +40 -8
- package/build/main/test/index.js.map +1 -1
- package/build/main/v1.generated.d.ts +193 -19
- package/build/main/v1.generated.js.map +1 -1
- package/build/migrations-v2/0000000000031-ui-permission-role-simplification.ts +70 -0
- package/build/migrations-v2/0000000000032-tenant-delete-ownership-cascades.ts +44 -0
- package/build/migrations-v2/0000000000033-plaid-connect-session-telemetry.ts +31 -0
- package/build/migrations-v2/0000000000033-reservation-issue-seed-bucket-index.ts +19 -0
- package/build/migrations-v2/0000000000034-retire-recent-classification-indexes.ts +29 -0
- package/build/migrations-v2/0000000000035-core-change-autovacuum.ts +24 -0
- package/build/migrations-v2/0000000000036-journal-entry-inactive-reason-constraint.ts +33 -0
- package/build/migrations-v2/0000000000037-journal-entry-inactive-reasons.ts +19 -0
- package/build/migrations-v2/0000000000038-journal-entry-group-inactive-reason.ts +14 -0
- package/build/migrations-v2/0000000000039-reservation-guest-breakdown.ts +19 -0
- package/build/migrations-v2/0000000000040-csv-import-source-identity-index.ts +23 -0
- package/build/migrations-v2/0000000000041-payment-returns.ts +38 -0
- package/build/migrations-v2/0000000000042-operation-resource-types.ts +609 -0
- package/build/migrations-v2/0000000000043-reservation-journal-freshness.ts +149 -0
- package/build/migrations-v2/0000000000044-reservation-journal-freshness-backfill.ts +254 -0
- package/build/migrations-v2/0000000000045-reservation-journal-freshness-revision-repair.ts +83 -0
- package/build/migrations-v2/0000000000046-materialized-reservation-journal-operations.ts +709 -0
- package/build/migrations-v2/0000000000047-public-audit-completion.ts +77 -0
- package/build/migrations-v2/0000000000048-reservation-journal-full-refresh-repair.ts +136 -0
- package/build/migrations-v2/0000000000049-queued-journal-operation-failures.ts +60 -0
- package/build/migrations-v2/0000000000050-connection-oauth-state.ts +43 -0
- package/build/migrations-v2/0000000000051-owner-statement-email-delivery.ts +134 -0
- package/build/migrations-v2/0000000000052-fee-journal-lock-skip-operations.ts +59 -0
- package/build/migrations-v2/0000000000053-fee-journal-lock-skip-null-attempt.ts +44 -0
- package/build/migrations-v2/0000000000054-journal-refresh-issue-codes.ts +16 -0
- package/build/migrations-v2/0000000000055-tenant-billing-subscription.ts +8 -0
- package/build/migrations-v2/0000000000056-journal-operation-failure-precedence.ts +306 -0
- package/build/migrations-v2/0000000000057-journal-invalidation-failure-code.ts +28 -0
- package/build/migrations-v2/0000000000058-journal-invalidation-locked-failure.ts +16 -0
- package/build/migrations-v2/0000000000059-billing-active-listing-cross-region-partner.ts +135 -0
- package/build/migrations-v2/0000000000060-accounting-integrity-change-driven.ts +99 -0
- package/build/migrations-v2/0000000000061-tenant-pending-onboarding-status.ts +37 -0
- package/build/migrations-v2/0000000000062-statement-payout-operations.ts +16 -0
- package/build/migrations-v2/0000000000063-missing-account-assignments-issue-code.ts +13 -0
- package/build/module/control-plane/index.d.ts +28 -6
- package/build/module/control-plane/index.js.map +1 -1
- package/build/module/index.d.ts +23 -1
- package/build/module/index.js +206 -50
- package/build/module/index.js.map +1 -1
- package/build/module/local/index.js +115 -8
- package/build/module/local/index.js.map +1 -1
- package/build/module/postgres-error.d.ts +3 -3
- package/build/module/postgres-error.js +4 -3
- package/build/module/postgres-error.js.map +1 -1
- package/build/module/query/tenant/status.d.ts +1 -1
- package/build/module/query/tenant/status.js +5 -2
- package/build/module/query/tenant/status.js.map +1 -1
- package/build/module/regional.d.ts +1 -0
- package/build/module/regional.js +1 -0
- package/build/module/regional.js.map +1 -1
- package/build/module/test/index.d.ts +25 -2
- package/build/module/test/index.js +39 -8
- package/build/module/test/index.js.map +1 -1
- package/build/module/v1.generated.d.ts +193 -19
- package/build/module/v1.generated.js.map +1 -1
- 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> {}
|