feat: manage owner usage history records
This commit is contained in:
@@ -6,7 +6,7 @@ TypeScript/Fastify API and PostgreSQL database layer for the current CONDO owner
|
||||
|
||||
- Database: existing `booking_test`.
|
||||
- Isolated schema: `condon`.
|
||||
- Migrations: `001_create_condon_schema`, `002_legacy_import_and_bookings`.
|
||||
- Migrations: `001_create_condon_schema`, `002_legacy_import_and_bookings`, `003_delete_usage_record`.
|
||||
- Reference data: 11 room types.
|
||||
- Imported latest `(2)` batch: 388 owners, 389 periods, 155 bookings, 157 usage records and 547 ledger rows.
|
||||
- Historical data import: `legacy-016dc36d15cc40a5` completed after local preflight and read-only verification.
|
||||
@@ -31,6 +31,7 @@ The database functions are:
|
||||
- `condon.open_entitlement_period`
|
||||
- `condon.create_usage_record`
|
||||
- `condon.create_usage_record_v2`
|
||||
- `condon.delete_usage_record`
|
||||
|
||||
`create_usage_record_v2` is the authoritative new-record write path. It calculates Night, Multiplier, Use and Balance, locks the entitlement period and writes the usage row, ledger row and new balance atomically. Historical rows use `legacy-source`, preserve source Use/Balance/Room/raw room type, and may have a null multiplier.
|
||||
|
||||
@@ -47,6 +48,7 @@ The database functions are:
|
||||
| GET | `/owner-accounts/:id` | Account detail and period balance |
|
||||
| GET | `/usage-records` | Searchable, paginated usage history |
|
||||
| POST | `/usage-records` | Transactional usage deduction |
|
||||
| DELETE | `/usage-records/:id` | Transactional usage deletion and balance restoration |
|
||||
| GET | `/dashboard` | Annual totals and room-type/month aggregates |
|
||||
|
||||
Runtime OpenAPI documentation is available at `/docs`.
|
||||
@@ -130,7 +132,7 @@ The database integration test covers all 100 automatic room-type combinations, b
|
||||
|
||||
## Rollback
|
||||
|
||||
The latest migration rollback is [002_legacy_import_and_bookings.down.sql](./migrations/002_legacy_import_and_bookings.down.sql). Do not run it after business data has been imported. The original empty-schema rollback remains [001_create_condon_schema.down.sql](./migrations/001_create_condon_schema.down.sql).
|
||||
The latest migration rollback is [003_delete_usage_record.down.sql](./migrations/003_delete_usage_record.down.sql). Do not run it after business data has been imported. The previous legacy rollback remains [002_legacy_import_and_bookings.down.sql](./migrations/002_legacy_import_and_bookings.down.sql), and the original empty-schema rollback remains [001_create_condon_schema.down.sql](./migrations/001_create_condon_schema.down.sql).
|
||||
|
||||
Do not run rollback after business data has been imported. Rollback is an explicit operator action and is not exposed as a normal npm command.
|
||||
|
||||
@@ -139,5 +141,5 @@ Do not run rollback after business data has been imported. Rollback is an explic
|
||||
- Importing a newer workbook without a new preflight batch.
|
||||
- Switching the static frontend to the API.
|
||||
- Multi-user identities, roles and backend-restart-persistent sessions.
|
||||
- Usage cancellation, reversal and manual balance adjustment.
|
||||
- Manual balance adjustment.
|
||||
- Cross-entitlement-year stays.
|
||||
|
||||
2
backend/migrations/003_delete_usage_record.down.sql
Normal file
2
backend/migrations/003_delete_usage_record.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
REVOKE ALL ON FUNCTION condon.delete_usage_record(uuid) FROM PUBLIC;
|
||||
DROP FUNCTION condon.delete_usage_record(uuid);
|
||||
167
backend/migrations/003_delete_usage_record.up.sql
Normal file
167
backend/migrations/003_delete_usage_record.up.sql
Normal file
@@ -0,0 +1,167 @@
|
||||
-- Add an atomic usage-record deletion path. The operation restores the
|
||||
-- entitlement period and reflows stored balance snapshots for later rows.
|
||||
|
||||
CREATE FUNCTION condon.delete_usage_record(
|
||||
p_usage_record_id uuid
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SECURITY INVOKER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_usage condon.usage_records%ROWTYPE;
|
||||
v_period condon.entitlement_periods%ROWTYPE;
|
||||
v_initial_balance integer;
|
||||
v_now timestamptz := pg_catalog.clock_timestamp();
|
||||
BEGIN
|
||||
IF p_usage_record_id IS NULL THEN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = '22023',
|
||||
MESSAGE = 'CONDON_USAGE_RECORD_ID_REQUIRED';
|
||||
END IF;
|
||||
|
||||
SELECT *
|
||||
INTO v_usage
|
||||
FROM condon.usage_records
|
||||
WHERE id = p_usage_record_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = 'P0002',
|
||||
MESSAGE = 'CONDON_USAGE_RECORD_NOT_FOUND';
|
||||
END IF;
|
||||
|
||||
SELECT *
|
||||
INTO v_period
|
||||
FROM condon.entitlement_periods
|
||||
WHERE id = v_usage.entitlement_period_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION USING
|
||||
ERRCODE = 'P0002',
|
||||
MESSAGE = 'CONDON_ENTITLEMENT_PERIOD_NOT_FOUND';
|
||||
END IF;
|
||||
|
||||
DELETE FROM condon.entitlement_ledger
|
||||
WHERE usage_record_id = v_usage.id;
|
||||
|
||||
DELETE FROM condon.usage_records
|
||||
WHERE id = v_usage.id;
|
||||
|
||||
v_initial_balance := v_period.annual_grant + v_period.carry_forward;
|
||||
|
||||
WITH ordered AS (
|
||||
SELECT
|
||||
ur.id,
|
||||
(
|
||||
v_initial_balance
|
||||
- COALESCE(
|
||||
SUM(ur.use_nights) OVER (
|
||||
ORDER BY
|
||||
CASE WHEN ur.source_sheet IS NULL THEN 1 ELSE 0 END,
|
||||
ur.source_sheet NULLS LAST,
|
||||
ur.source_sequence NULLS LAST,
|
||||
ur.source_row NULLS LAST,
|
||||
ur.created_at ASC,
|
||||
ur.id ASC
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
||||
),
|
||||
0
|
||||
)
|
||||
)::integer AS balance_before,
|
||||
(
|
||||
v_initial_balance
|
||||
- SUM(ur.use_nights) OVER (
|
||||
ORDER BY
|
||||
CASE WHEN ur.source_sheet IS NULL THEN 1 ELSE 0 END,
|
||||
ur.source_sheet NULLS LAST,
|
||||
ur.source_sequence NULLS LAST,
|
||||
ur.source_row NULLS LAST,
|
||||
ur.created_at ASC,
|
||||
ur.id ASC
|
||||
)
|
||||
)::integer AS balance_after
|
||||
FROM condon.usage_records AS ur
|
||||
WHERE ur.entitlement_period_id = v_period.id
|
||||
)
|
||||
UPDATE condon.usage_records AS ur
|
||||
SET
|
||||
balance_before = ordered.balance_before,
|
||||
balance_after = ordered.balance_after
|
||||
FROM ordered
|
||||
WHERE ur.id = ordered.id;
|
||||
|
||||
WITH ordered AS (
|
||||
SELECT
|
||||
ur.id,
|
||||
(
|
||||
v_initial_balance
|
||||
- COALESCE(
|
||||
SUM(ur.use_nights) OVER (
|
||||
ORDER BY
|
||||
CASE WHEN ur.source_sheet IS NULL THEN 1 ELSE 0 END,
|
||||
ur.source_sheet NULLS LAST,
|
||||
ur.source_sequence NULLS LAST,
|
||||
ur.source_row NULLS LAST,
|
||||
ur.created_at ASC,
|
||||
ur.id ASC
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
||||
),
|
||||
0
|
||||
)
|
||||
)::integer AS balance_before,
|
||||
(
|
||||
v_initial_balance
|
||||
- SUM(ur.use_nights) OVER (
|
||||
ORDER BY
|
||||
CASE WHEN ur.source_sheet IS NULL THEN 1 ELSE 0 END,
|
||||
ur.source_sheet NULLS LAST,
|
||||
ur.source_sequence NULLS LAST,
|
||||
ur.source_row NULLS LAST,
|
||||
ur.created_at ASC,
|
||||
ur.id ASC
|
||||
)
|
||||
)::integer AS balance_after
|
||||
FROM condon.usage_records AS ur
|
||||
WHERE ur.entitlement_period_id = v_period.id
|
||||
)
|
||||
UPDATE condon.entitlement_ledger AS ledger
|
||||
SET
|
||||
balance_before = ordered.balance_before,
|
||||
balance_after = ordered.balance_after
|
||||
FROM ordered
|
||||
WHERE ledger.usage_record_id = ordered.id
|
||||
AND ledger.entry_type = 'usage';
|
||||
|
||||
UPDATE condon.entitlement_periods
|
||||
SET
|
||||
current_balance = (
|
||||
v_initial_balance
|
||||
- COALESCE(
|
||||
(
|
||||
SELECT SUM(ur.use_nights)
|
||||
FROM condon.usage_records AS ur
|
||||
WHERE ur.entitlement_period_id = v_period.id
|
||||
),
|
||||
0
|
||||
)
|
||||
)::integer,
|
||||
row_version = row_version + 1,
|
||||
updated_at = v_now
|
||||
WHERE id = v_period.id;
|
||||
|
||||
DELETE FROM condon.bookings AS booking
|
||||
WHERE booking.confirmation_no = v_usage.confirmation_no
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM condon.usage_records AS ur
|
||||
WHERE ur.confirmation_no = booking.confirmation_no
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
|
||||
REVOKE ALL ON FUNCTION condon.delete_usage_record(uuid) FROM PUBLIC;
|
||||
@@ -26,12 +26,24 @@ const legacyDownPath = path.join(
|
||||
"migrations",
|
||||
"002_legacy_import_and_bookings.down.sql"
|
||||
);
|
||||
const deleteUpPath = path.join(
|
||||
backendDirectory,
|
||||
"migrations",
|
||||
"003_delete_usage_record.up.sql"
|
||||
);
|
||||
const deleteDownPath = path.join(
|
||||
backendDirectory,
|
||||
"migrations",
|
||||
"003_delete_usage_record.down.sql"
|
||||
);
|
||||
|
||||
const [upSql, downSql, legacyUpSql, legacyDownSql] = await Promise.all([
|
||||
const [upSql, downSql, legacyUpSql, legacyDownSql, deleteUpSql, deleteDownSql] = await Promise.all([
|
||||
readFile(upPath, "utf8"),
|
||||
readFile(downPath, "utf8"),
|
||||
readFile(legacyUpPath, "utf8"),
|
||||
readFile(legacyDownPath, "utf8")
|
||||
readFile(legacyDownPath, "utf8"),
|
||||
readFile(deleteUpPath, "utf8"),
|
||||
readFile(deleteDownPath, "utf8")
|
||||
]);
|
||||
|
||||
const checks = [];
|
||||
@@ -166,6 +178,21 @@ record(
|
||||
&& /^DROP FUNCTION condon\.create_usage_record_v2/gm.test(legacyDownSql)
|
||||
&& /^DROP INDEX condon\.usage_records_source_location_key;$/gm.test(legacyDownSql)
|
||||
);
|
||||
record(
|
||||
"delete migration is an internal transactional function",
|
||||
/^CREATE FUNCTION condon\.delete_usage_record\(\s*p_usage_record_id uuid\s*\)/m.test(deleteUpSql)
|
||||
&& /^RETURNS void$/m.test(deleteUpSql)
|
||||
&& /^SECURITY INVOKER$/m.test(deleteUpSql)
|
||||
&& deleteUpSql.includes("FOR UPDATE")
|
||||
&& deleteUpSql.includes("DELETE FROM condon.entitlement_ledger")
|
||||
&& deleteUpSql.includes("UPDATE condon.entitlement_periods")
|
||||
&& /^REVOKE ALL ON FUNCTION condon\.delete_usage_record\(uuid\) FROM PUBLIC;$/m.test(deleteUpSql)
|
||||
);
|
||||
record(
|
||||
"delete migration down reverses its function",
|
||||
/^DROP FUNCTION condon\.delete_usage_record\(uuid\);$/m.test(deleteDownSql)
|
||||
&& !/\bCASCADE\b/i.test(deleteDownSql)
|
||||
);
|
||||
|
||||
for (const check of checks) {
|
||||
process.stdout.write(
|
||||
|
||||
@@ -166,6 +166,7 @@ export class ImportSnapshotRepository {
|
||||
));
|
||||
this.ownerById = new Map(this.owners.map(owner => [owner.id, owner]));
|
||||
this.periodBalances = new Map();
|
||||
this.periodInitialBalances = new Map();
|
||||
this.periodYears = new Set();
|
||||
this.idempotentResponses = new Map();
|
||||
|
||||
@@ -183,6 +184,7 @@ export class ImportSnapshotRepository {
|
||||
const rows2025 = usageByRoomAndYear.get(`${room}|2025`) ?? [];
|
||||
const carryForward = rows2025.length > 0 ? Number(rows2025.at(-1).balance) : 0;
|
||||
this.periodYears.add(periodKey(owner.id, 2026));
|
||||
this.periodInitialBalances.set(periodKey(owner.id, 2026), 15 + carryForward);
|
||||
const used2026 = (usageByRoomAndYear.get(`${room}|2026`) ?? [])
|
||||
.reduce((sum, record) => sum + Number(record.use), 0);
|
||||
this.periodBalances.set(periodKey(owner.id, 2026), 15 + carryForward - used2026);
|
||||
@@ -195,6 +197,7 @@ export class ImportSnapshotRepository {
|
||||
for (const year of otherYears) {
|
||||
const rows = usageByRoomAndYear.get(`${room}|${year}`) ?? [];
|
||||
this.periodYears.add(periodKey(owner.id, year));
|
||||
this.periodInitialBalances.set(periodKey(owner.id, year), 15);
|
||||
this.periodBalances.set(
|
||||
periodKey(owner.id, year),
|
||||
15 - rows.reduce((sum, record) => sum + Number(record.use), 0)
|
||||
@@ -252,7 +255,7 @@ export class ImportSnapshotRepository {
|
||||
status: "ok",
|
||||
database: "booking_test",
|
||||
schema: "condon",
|
||||
migrationVersion: "002_legacy_import_and_bookings:snapshot"
|
||||
migrationVersion: "003_delete_usage_record:snapshot"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -303,6 +306,38 @@ export class ImportSnapshotRepository {
|
||||
return paginate(filtered.map(entry => ({ ...entry.publicRecord })), query);
|
||||
}
|
||||
|
||||
rebuildPeriodBalance(ownerAccountId, periodYear) {
|
||||
const key = periodKey(ownerAccountId, periodYear);
|
||||
const entries = this.usageEntries
|
||||
.filter(entry => entry.publicRecord.ownerAccountId === ownerAccountId && entry.periodYear === periodYear)
|
||||
.sort((left, right) => {
|
||||
const leftImported = left.sourceSheet !== null;
|
||||
const rightImported = right.sourceSheet !== null;
|
||||
if (leftImported !== rightImported) return leftImported ? -1 : 1;
|
||||
if (leftImported) return sourceOrder(left, right);
|
||||
return String(left.publicRecord.createdAt).localeCompare(String(right.publicRecord.createdAt))
|
||||
|| left.publicRecord.id.localeCompare(right.publicRecord.id);
|
||||
});
|
||||
let balance = this.periodInitialBalances.get(key) ?? 15;
|
||||
for (const entry of entries) {
|
||||
balance -= entry.publicRecord.use;
|
||||
entry.publicRecord.balance = balance;
|
||||
}
|
||||
this.periodBalances.set(key, balance);
|
||||
}
|
||||
|
||||
async deleteUsageRecord(id) {
|
||||
const index = this.usageEntries.findIndex(entry => entry.publicRecord.id === id);
|
||||
if (index < 0) {
|
||||
throw new ApiError(404, "NOT_FOUND", "The requested business record was not found");
|
||||
}
|
||||
const [removed] = this.usageEntries.splice(index, 1);
|
||||
for (const [key, value] of this.idempotentResponses) {
|
||||
if (value.response.id === id) this.idempotentResponses.delete(key);
|
||||
}
|
||||
this.rebuildPeriodBalance(removed.publicRecord.ownerAccountId, removed.periodYear);
|
||||
}
|
||||
|
||||
async createUsageRecord(input) {
|
||||
const previous = this.idempotentResponses.get(input.idempotencyKey);
|
||||
if (previous) {
|
||||
|
||||
@@ -17,6 +17,9 @@ const MIGRATION_CHECKSUM =
|
||||
const MIGRATION_V2 = "002_legacy_import_and_bookings";
|
||||
const MIGRATION_V2_CHECKSUM =
|
||||
"cfc38024984c883490cb46ab8ab1fca2e72c67f10dc3a43c179b12a8e6f2405b";
|
||||
const MIGRATION_V3 = "003_delete_usage_record";
|
||||
const MIGRATION_V3_CHECKSUM =
|
||||
"9110464c03ed18385fbc2da1d4369715fcbf83148d7a0f2550c25710a5f54148";
|
||||
|
||||
const expectedColumns = {
|
||||
bookings: [
|
||||
@@ -334,11 +337,12 @@ try {
|
||||
calculate_multiplier: { volatility: "s" },
|
||||
create_usage_record: { volatility: "v" },
|
||||
create_usage_record_v2: { volatility: "v" },
|
||||
delete_usage_record: { volatility: "v" },
|
||||
ensure_booking_for_usage: { volatility: "v" },
|
||||
open_entitlement_period: { volatility: "v" }
|
||||
};
|
||||
const functionsValid =
|
||||
functionResult.rowCount === 5
|
||||
functionResult.rowCount === 6
|
||||
&& functionResult.rows.every(row =>
|
||||
expectedFunctions[row.function_name]?.volatility === row.volatility
|
||||
&& row.security_definer === false
|
||||
@@ -371,13 +375,16 @@ try {
|
||||
&& businessCounts.usage_record_count === 157
|
||||
&& businessCounts.ledger_count === 547,
|
||||
migrationVerified:
|
||||
migrationResult.rowCount === 2
|
||||
migrationResult.rowCount === 3
|
||||
&& migrationResult.rows.some(row =>
|
||||
row.version === MIGRATION_VERSION && row.checksum === MIGRATION_CHECKSUM
|
||||
)
|
||||
&& migrationResult.rows.some(row =>
|
||||
row.version === MIGRATION_V2 && row.checksum === MIGRATION_V2_CHECKSUM
|
||||
)
|
||||
&& migrationResult.rows.some(row =>
|
||||
row.version === MIGRATION_V3 && row.checksum === MIGRATION_V3_CHECKSUM
|
||||
)
|
||||
};
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
|
||||
@@ -31,6 +31,9 @@ const ownerQuerySchema = z.object({
|
||||
const ownerParamsSchema = z.object({
|
||||
id: z.string().uuid()
|
||||
});
|
||||
const usageRecordParamsSchema = z.object({
|
||||
id: z.string().uuid()
|
||||
});
|
||||
const ownerDetailQuerySchema = z.object({
|
||||
year: z.coerce.number().int().min(2000).max(9999).default(currentYear)
|
||||
});
|
||||
@@ -193,7 +196,7 @@ export async function buildApp(
|
||||
"http://localhost:4173"
|
||||
],
|
||||
credentials: true,
|
||||
methods: ["GET", "POST"]
|
||||
methods: ["GET", "POST", "DELETE"]
|
||||
});
|
||||
|
||||
const publicAuthPaths = new Set([
|
||||
@@ -517,6 +520,30 @@ export async function buildApp(
|
||||
return reply.code(201).send(result);
|
||||
});
|
||||
|
||||
app.delete("/usage-records/:id", {
|
||||
schema: {
|
||||
tags: ["usage-records"],
|
||||
params: {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid" }
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: { type: "null" },
|
||||
400: errorResponseSchema,
|
||||
404: errorResponseSchema,
|
||||
409: errorResponseSchema,
|
||||
500: errorResponseSchema
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const params = parse(usageRecordParamsSchema, request.params);
|
||||
await options.repository.deleteUsageRecord(params.id);
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
app.get("/dashboard", {
|
||||
schema: {
|
||||
tags: ["dashboard"],
|
||||
|
||||
@@ -43,6 +43,7 @@ export function mapDatabaseError(error: unknown): ApiError {
|
||||
if (
|
||||
message === "CONDON_OWNER_ACCOUNT_NOT_FOUND"
|
||||
|| message === "CONDON_ENTITLEMENT_PERIOD_NOT_FOUND"
|
||||
|| message === "CONDON_USAGE_RECORD_NOT_FOUND"
|
||||
|| message.endsWith("_ROOM_TYPE_NOT_FOUND")
|
||||
) {
|
||||
return new ApiError(404, "NOT_FOUND", "The requested business record was not found");
|
||||
|
||||
@@ -353,6 +353,17 @@ export class PostgresCondoRepository implements CondoRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUsageRecord(id: string): Promise<void> {
|
||||
try {
|
||||
await this.pool.query(
|
||||
"SELECT condon.delete_usage_record($1::uuid)",
|
||||
[id]
|
||||
);
|
||||
} catch (error) {
|
||||
throw mapDatabaseError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getDashboard(periodYear: number): Promise<DashboardResult> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface CondoRepository {
|
||||
getOwnerAccount(id: string, periodYear: number): Promise<OwnerAccount | null>;
|
||||
listUsageRecords(query: UsageRecordQuery): Promise<UsageRecordList>;
|
||||
createUsageRecord(input: CreateUsageRecordInput): Promise<UsageRecord>;
|
||||
deleteUsageRecord(id: string): Promise<void>;
|
||||
getDashboard(periodYear: number): Promise<DashboardResult>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ function createRepository(
|
||||
createdAt: "2026-07-29T00:00:00.000Z"
|
||||
};
|
||||
},
|
||||
async deleteUsageRecord() {},
|
||||
async getDashboard(periodYear: number) {
|
||||
return {
|
||||
periodYear,
|
||||
@@ -389,6 +390,34 @@ test("business errors retain safe status and code", async t => {
|
||||
assert.equal(JSON.stringify(response.json()).includes("password"), false);
|
||||
});
|
||||
|
||||
test("usage deletion validates the record id and returns no content", async t => {
|
||||
let captured: string | undefined;
|
||||
const repository = createRepository({
|
||||
async deleteUsageRecord(id) {
|
||||
captured = id;
|
||||
}
|
||||
});
|
||||
const { app, cookie } = await buildAuthenticatedApp(repository);
|
||||
t.after(() => app.close());
|
||||
|
||||
const response = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/usage-records/${usageId}`,
|
||||
headers: { cookie }
|
||||
});
|
||||
assert.equal(response.statusCode, 204);
|
||||
assert.equal(response.body, "");
|
||||
assert.equal(captured, usageId);
|
||||
|
||||
const malformed = await app.inject({
|
||||
method: "DELETE",
|
||||
url: "/usage-records/not-a-uuid",
|
||||
headers: { cookie }
|
||||
});
|
||||
assert.equal(malformed.statusCode, 400);
|
||||
assert.equal(malformed.json().error.code, "VALIDATION_ERROR");
|
||||
});
|
||||
|
||||
test("dashboard and OpenAPI expose the planned surface", async t => {
|
||||
const { app, cookie } = await buildAuthenticatedApp(createRepository());
|
||||
t.after(() => app.close());
|
||||
@@ -412,6 +441,7 @@ test("dashboard and OpenAPI expose the planned surface", async t => {
|
||||
"/owner-accounts",
|
||||
"/owner-accounts/{id}",
|
||||
"/room-types",
|
||||
"/usage-records"
|
||||
"/usage-records",
|
||||
"/usage-records/{id}"
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user