fix(tenancies-audit): resolve findings from 7-agent system-wide rename audit
MUST-FIX:
- src/app/api/v1/admin/users/[id]/permission-overrides/route.ts:70 — the
PUT allowlist still gated `reservations: {view,create,activate,cancel}`.
Stale: would reject valid `tenancies.{view,manage,cancel}` writes and
silently accept ghost `reservations.*` writes that never land. Replaced.
- src/lib/services/alert-rules.ts:68 — `reservation.no_agreement` alert
emitted `entityType: 'reservation'`. Every other tenancy-related
audit/socket/dashboard label is `'berth_tenancy'`. Inconsistent dedupe
+ activity-feed label miss.
- tests/e2e/exhaustive/08-portal.spec.ts:6 — hardcoded /portal/my-reservations
navigates to a 404 every run.
- tests/e2e/exhaustive/03-reservations.spec.ts — entire spec renamed to
03-tenancies.spec.ts; tab + button locators updated to match renamed UI.
SHOULD-FIX (consistency):
- src/components/clients/client-detail.tsx — useRealtimeInvalidation only
caught 3 of the 4 berth_tenancy:* events; added the `:created` listener.
- src/lib/services/client-merge.service.ts — MergeResult.movedRows.reservations
+ snapshot.reservations + local loserReservations / movedReservations
renamed to tenancies / loserTenancies / movedTenancies. No external
consumers grep-confirmed.
- src/lib/services/gdpr-bundle-builder.ts — GdprBundle.reservations field
renamed to .tenancies; user-facing HTML section "Reservations" → "Tenancies";
local reservationRows → tenancyRows.
- 6 UI copy strings: gdpr-export-button, bulk-archive-wizard,
bulk-hard-delete-dialog, hard-delete-dialog, admin-sections-browser ×2,
admin/import/page, won-status-panel — all "reservations" prose updated
to "tenancies" (occupancy-record sense).
- tests/integration/api/tenancies.test.ts — handler import aliases
`createReservationHandler` etc renamed to `createTenancyHandler` etc.
- tests/unit/services/berth-tenancies.test.ts — local helper makeReservation
→ makeTenancyLocal (avoids shadow of the renamed factory).
- scripts/audit-permissions.ts — stale allowlist entry for
/berth-reservations/[id]/route.ts removed (path no longer exists).
- docs/runbooks/permission-audit.md — stale row for same path removed.
- docs/tenancies-design.md — fixed factual error
("tenancies.service.ts" → "berth-tenancies.service.ts").
Verified: tsc clean, 1493/1493 vitest.
Dev-server note: the running `next dev` process started before P2 and
shows Turbopack cached compile errors against the renamed schema files.
Source is correct (./tenancies); restart `next dev` to clear the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,6 @@ Scanned 182 route files under `src/app/api/v1/`.
|
|||||||
| `src/app/api/v1/alerts/[id]/dismiss/route.ts` | POST | Alerts are user-scoped; port-filtered via auth context. |
|
| `src/app/api/v1/alerts/[id]/dismiss/route.ts` | POST | Alerts are user-scoped; port-filtered via auth context. |
|
||||||
| `src/app/api/v1/alerts/count/route.ts` | GET | Alerts are user-scoped; port-filtered via auth context. |
|
| `src/app/api/v1/alerts/count/route.ts` | GET | Alerts are user-scoped; port-filtered via auth context. |
|
||||||
| `src/app/api/v1/alerts/route.ts` | GET | Alerts are user-scoped; port-filtered via auth context. |
|
| `src/app/api/v1/alerts/route.ts` | GET | Alerts are user-scoped; port-filtered via auth context. |
|
||||||
| `src/app/api/v1/berth-reservations/[id]/route.ts` | PATCH | TODO: PATCH should map to reservations:edit (not currently in catalog). |
|
|
||||||
| `src/app/api/v1/currency/convert/route.ts` | POST | Currency reference data; port-scoped, no PII. |
|
| `src/app/api/v1/currency/convert/route.ts` | POST | Currency reference data; port-scoped, no PII. |
|
||||||
| `src/app/api/v1/currency/rates/refresh/route.ts` | POST | TODO: gate with admin:manage_settings — currently allow-listed. |
|
| `src/app/api/v1/currency/rates/refresh/route.ts` | POST | TODO: gate with admin:manage_settings — currently allow-listed. |
|
||||||
| `src/app/api/v1/currency/rates/route.ts` | GET | Currency reference data; port-scoped, no PII. |
|
| `src/app/api/v1/currency/rates/route.ts` | GET | Currency reference data; port-scoped, no PII. |
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ The `tenancies_module` integration check resolves to `tenancies_module_enabled =
|
|||||||
|
|
||||||
## Service layer additions
|
## Service layer additions
|
||||||
|
|
||||||
`src/lib/services/tenancies.service.ts` (renamed from `berth-reservations.service.ts`):
|
`src/lib/services/berth-tenancies.service.ts` (renamed from `berth-reservations.service.ts`):
|
||||||
|
|
||||||
- `listTenancies({ portId, filters, page })` — gated read.
|
- `listTenancies({ portId, filters, page })` — gated read.
|
||||||
- `createTenancy(portId, data, meta)` — mints a row; also triggers the module-enable flip on first insert.
|
- `createTenancy(portId, data, meta)` — mints a row; also triggers the module-enable flip on first insert.
|
||||||
|
|||||||
@@ -73,10 +73,6 @@ const ALLOW_LIST: ReadonlyArray<{ pattern: RegExp; reason: string }> = [
|
|||||||
pattern: /\/custom-fields\/\[entityId\]\//,
|
pattern: /\/custom-fields\/\[entityId\]\//,
|
||||||
reason: 'TODO: needs custom_fields:* permission. PUT path internally validated.',
|
reason: 'TODO: needs custom_fields:* permission. PUT path internally validated.',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pattern: /\/berth-reservations\/\[id\]\/route\.ts$/,
|
|
||||||
reason: 'TODO: PATCH should map to reservations:edit (not currently in catalog).',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Finding {
|
interface Finding {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function DataImportPage() {
|
|||||||
<li>Dry-run preview that shows new vs. matched-existing rows before commit.</li>
|
<li>Dry-run preview that shows new vs. matched-existing rows before commit.</li>
|
||||||
<li>Conflict-resolution choices (skip, update, dedup-by-email) per import type.</li>
|
<li>Conflict-resolution choices (skip, update, dedup-by-email) per import type.</li>
|
||||||
<li>Per-port import history with rollback.</li>
|
<li>Per-port import history with rollback.</li>
|
||||||
<li>Templates for clients, yachts, companies, berths, reservations, expenses.</li>
|
<li>Templates for clients, yachts, companies, berths, tenancies, expenses.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-xs text-muted-foreground pt-2">
|
<p className="text-xs text-muted-foreground pt-2">
|
||||||
Imports run against the BullMQ <code>import</code> queue (concurrency 1) so partial
|
Imports run against the BullMQ <code>import</code> queue (concurrency 1) so partial
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const ALLOWED_RESOURCE_ACTIONS: Record<string, Set<string>> = {
|
|||||||
yachts: new Set(['view', 'create', 'edit', 'delete', 'transfer']),
|
yachts: new Set(['view', 'create', 'edit', 'delete', 'transfer']),
|
||||||
companies: new Set(['view', 'create', 'edit', 'delete']),
|
companies: new Set(['view', 'create', 'edit', 'delete']),
|
||||||
memberships: new Set(['view', 'manage']),
|
memberships: new Set(['view', 'manage']),
|
||||||
reservations: new Set(['view', 'create', 'activate', 'cancel']),
|
tenancies: new Set(['view', 'manage', 'cancel']),
|
||||||
admin: new Set([
|
admin: new Set([
|
||||||
'manage_users',
|
'manage_users',
|
||||||
'view_audit_log',
|
'view_audit_log',
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const GROUPS: AdminGroup[] = [
|
|||||||
{
|
{
|
||||||
href: 'custom-fields',
|
href: 'custom-fields',
|
||||||
label: 'Custom Fields',
|
label: 'Custom Fields',
|
||||||
description: 'Tenant-defined fields for clients, yachts, and reservations.',
|
description: 'Tenant-defined fields for clients, yachts, and tenancies.',
|
||||||
icon: SlidersHorizontal,
|
icon: SlidersHorizontal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -261,7 +261,7 @@ const GROUPS: AdminGroup[] = [
|
|||||||
{
|
{
|
||||||
href: 'import',
|
href: 'import',
|
||||||
label: 'Bulk Import',
|
label: 'Bulk Import',
|
||||||
description: 'CSV-driven imports for clients, yachts, and reservations.',
|
description: 'CSV-driven imports for clients, yachts, and tenancies.',
|
||||||
icon: FileUp,
|
icon: FileUp,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
|
|||||||
|
|
||||||
<div className="rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground">
|
<div className="rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||||
Low-stakes defaults: release available/under-offer berths, keep sold ones, cancel
|
Low-stakes defaults: release available/under-offer berths, keep sold ones, cancel
|
||||||
reservations, leave invoices/signing requests alone. Yachts stay on the archived
|
tenancies, leave invoices/signing requests alone. Yachts stay on the archived
|
||||||
client. To customise per-client, archive that client individually instead.
|
client. To customise per-client, archive that client individually instead.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
|
|||||||
</p>
|
</p>
|
||||||
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900 text-xs">
|
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900 text-xs">
|
||||||
For each client we delete: client record + addresses, contacts, notes, tags, portal
|
For each client we delete: client record + addresses, contacts, notes, tags, portal
|
||||||
user, GDPR records, all interests, all reservations. Signed documents, email threads,
|
user, GDPR records, all interests, all tenancies. Signed documents, email threads, files
|
||||||
files and reminders are detached but kept.
|
and reminders are detached but kept.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) {
|
|||||||
'yacht:ownership_transferred': [['clients', clientId]],
|
'yacht:ownership_transferred': [['clients', clientId]],
|
||||||
'company_membership:added': [['clients', clientId]],
|
'company_membership:added': [['clients', clientId]],
|
||||||
'company_membership:ended': [['clients', clientId]],
|
'company_membership:ended': [['clients', clientId]],
|
||||||
|
'berth_tenancy:created': [['clients', clientId]],
|
||||||
'berth_tenancy:activated': [['clients', clientId]],
|
'berth_tenancy:activated': [['clients', clientId]],
|
||||||
'berth_tenancy:ended': [['clients', clientId]],
|
'berth_tenancy:ended': [['clients', clientId]],
|
||||||
'berth_tenancy:cancelled': [['clients', clientId]],
|
'berth_tenancy:cancelled': [['clients', clientId]],
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function GdprExportButton({ clientId }: { clientId: string }) {
|
|||||||
<DialogTitle>Personal data export</DialogTitle>
|
<DialogTitle>Personal data export</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Bundles every record we hold about this client (profile, contacts, addresses, yachts,
|
Bundles every record we hold about this client (profile, contacts, addresses, yachts,
|
||||||
companies, interests, reservations, invoices, documents, audit log) into a ZIP with JSON
|
companies, interests, tenancies, invoices, documents, audit log) into a ZIP with JSON
|
||||||
and HTML copies. Used to satisfy GDPR Article 15 access requests.
|
and HTML copies. Used to satisfy GDPR Article 15 access requests.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:
|
|||||||
<ul className="mt-1.5 list-disc pl-5 text-xs space-y-0.5">
|
<ul className="mt-1.5 list-disc pl-5 text-xs space-y-0.5">
|
||||||
<li>Client record + addresses, contacts, notes, tags</li>
|
<li>Client record + addresses, contacts, notes, tags</li>
|
||||||
<li>Portal user account + GDPR consent records</li>
|
<li>Portal user account + GDPR consent records</li>
|
||||||
<li>All pipeline interests + reservations for this client</li>
|
<li>All pipeline interests + tenancies for this client</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="font-medium mt-2">What is preserved</p>
|
<p className="font-medium mt-2">What is preserved</p>
|
||||||
<ul className="mt-1.5 list-disc pl-5 text-xs space-y-0.5">
|
<ul className="mt-1.5 list-disc pl-5 text-xs space-y-0.5">
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ export function WonStatusPanel({ interestId, outcome }: WonStatusPanelProps) {
|
|||||||
Won - wrap-up checklist
|
Won - wrap-up checklist
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="text-xs text-emerald-800/80">
|
<p className="text-xs text-emerald-800/80">
|
||||||
Upload anything that didn't flow through the system automatically. Reservations,
|
Upload anything that didn't flow through the system automatically. Tenancies, deposit
|
||||||
deposit invoicing, and client billing are handled outside the CRM - this checklist is for
|
invoicing, and client billing are handled outside the CRM - this checklist is for the
|
||||||
the paperwork that lives on the deal itself.
|
paperwork that lives on the deal itself.
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async function reservationNoAgreement(portId: string): Promise<AlertCandidate[]>
|
|||||||
title: `Reservation needs an agreement`,
|
title: `Reservation needs an agreement`,
|
||||||
body: `Active reservation for ${r.yachtName} (${r.clientName}) has no signed agreement yet.`,
|
body: `Active reservation for ${r.yachtName} (${r.clientName}) has no signed agreement yet.`,
|
||||||
link: `/[port]/tenancies/${r.id}`,
|
link: `/[port]/tenancies/${r.id}`,
|
||||||
entityType: 'reservation',
|
entityType: 'berth_tenancy',
|
||||||
entityId: r.id,
|
entityId: r.id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export interface MergeResult {
|
|||||||
notes: number;
|
notes: number;
|
||||||
tags: number;
|
tags: number;
|
||||||
relationships: number;
|
relationships: number;
|
||||||
reservations: number;
|
tenancies: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
|
|||||||
.select({ id: interests.id })
|
.select({ id: interests.id })
|
||||||
.from(interests)
|
.from(interests)
|
||||||
.where(eq(interests.clientId, opts.loserId));
|
.where(eq(interests.clientId, opts.loserId));
|
||||||
const loserReservations = await tx
|
const loserTenancies = await tx
|
||||||
.select({ id: berthTenancies.id })
|
.select({ id: berthTenancies.id })
|
||||||
.from(berthTenancies)
|
.from(berthTenancies)
|
||||||
.where(eq(berthTenancies.clientId, opts.loserId));
|
.where(eq(berthTenancies.clientId, opts.loserId));
|
||||||
@@ -172,7 +172,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
|
|||||||
notes: loserNotes,
|
notes: loserNotes,
|
||||||
tags: loserTags,
|
tags: loserTags,
|
||||||
interests: loserInterests.map((r) => r.id),
|
interests: loserInterests.map((r) => r.id),
|
||||||
reservations: loserReservations.map((r) => r.id),
|
tenancies: loserTenancies.map((r) => r.id),
|
||||||
relationshipsAsA: loserRelationshipsAsA,
|
relationshipsAsA: loserRelationshipsAsA,
|
||||||
relationshipsAsB: loserRelationshipsAsB,
|
relationshipsAsB: loserRelationshipsAsB,
|
||||||
fieldChoices: opts.fieldChoices ?? {},
|
fieldChoices: opts.fieldChoices ?? {},
|
||||||
@@ -213,7 +213,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
|
|||||||
.returning({ id: interests.id })
|
.returning({ id: interests.id })
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
const movedReservations = (
|
const movedTenancies = (
|
||||||
await tx
|
await tx
|
||||||
.update(berthTenancies)
|
.update(berthTenancies)
|
||||||
.set({ clientId: opts.winnerId, updatedAt: new Date() })
|
.set({ clientId: opts.winnerId, updatedAt: new Date() })
|
||||||
@@ -357,7 +357,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
|
|||||||
loserId: opts.loserId,
|
loserId: opts.loserId,
|
||||||
loserName: loserRow.fullName,
|
loserName: loserRow.fullName,
|
||||||
movedInterests,
|
movedInterests,
|
||||||
movedReservations,
|
movedTenancies,
|
||||||
movedContacts,
|
movedContacts,
|
||||||
movedAddresses,
|
movedAddresses,
|
||||||
},
|
},
|
||||||
@@ -372,7 +372,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
|
|||||||
notes: movedNotes,
|
notes: movedNotes,
|
||||||
tags: movedTags,
|
tags: movedTags,
|
||||||
relationships: movedRelationships,
|
relationships: movedRelationships,
|
||||||
reservations: movedReservations,
|
tenancies: movedTenancies,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export interface GdprBundle {
|
|||||||
}>;
|
}>;
|
||||||
interests: Record<string, unknown>[];
|
interests: Record<string, unknown>[];
|
||||||
contactLog: Record<string, unknown>[];
|
contactLog: Record<string, unknown>[];
|
||||||
reservations: Record<string, unknown>[];
|
tenancies: Record<string, unknown>[];
|
||||||
invoices: Record<string, unknown>[];
|
invoices: Record<string, unknown>[];
|
||||||
documents: Record<string, unknown>[];
|
documents: Record<string, unknown>[];
|
||||||
files: Record<string, unknown>[];
|
files: Record<string, unknown>[];
|
||||||
@@ -95,7 +95,7 @@ export async function buildClientBundle(clientId: string, portId: string): Promi
|
|||||||
ownedYachts,
|
ownedYachts,
|
||||||
membershipRows,
|
membershipRows,
|
||||||
interestRows,
|
interestRows,
|
||||||
reservationRows,
|
tenancyRows,
|
||||||
invoiceRows,
|
invoiceRows,
|
||||||
documentRows,
|
documentRows,
|
||||||
fileRows,
|
fileRows,
|
||||||
@@ -268,7 +268,7 @@ export async function buildClientBundle(clientId: string, portId: string): Promi
|
|||||||
})),
|
})),
|
||||||
interests: interestRows.map(toJsonRow),
|
interests: interestRows.map(toJsonRow),
|
||||||
contactLog: contactLogRows.map(toJsonRow),
|
contactLog: contactLogRows.map(toJsonRow),
|
||||||
reservations: reservationRows.map(toJsonRow),
|
tenancies: tenancyRows.map(toJsonRow),
|
||||||
invoices: invoiceRows.map(toJsonRow),
|
invoices: invoiceRows.map(toJsonRow),
|
||||||
documents: documentRows.map(toJsonRow),
|
documents: documentRows.map(toJsonRow),
|
||||||
files: fileRows.map(toJsonRow),
|
files: fileRows.map(toJsonRow),
|
||||||
@@ -361,7 +361,7 @@ export function renderBundleHtml(bundle: GdprBundle): string {
|
|||||||
),
|
),
|
||||||
tableSection('Interests', bundle.interests),
|
tableSection('Interests', bundle.interests),
|
||||||
tableSection('Contact log', bundle.contactLog),
|
tableSection('Contact log', bundle.contactLog),
|
||||||
tableSection('Reservations', bundle.reservations),
|
tableSection('Tenancies', bundle.tenancies),
|
||||||
tableSection('Invoices', bundle.invoices),
|
tableSection('Invoices', bundle.invoices),
|
||||||
tableSection('Documents', bundle.documents),
|
tableSection('Documents', bundle.documents),
|
||||||
tableSection('Files', bundle.files),
|
tableSection('Files', bundle.files),
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { clickEverythingOnPage } from '../../helpers/click-everything';
|
import { clickEverythingOnPage } from '../../helpers/click-everything';
|
||||||
import { login, navigateTo, PORT_SLUG } from '../smoke/helpers';
|
import { login, navigateTo, PORT_SLUG } from '../smoke/helpers';
|
||||||
|
|
||||||
test.describe('exhaustive: berth reservations', () => {
|
test.describe('exhaustive: berth tenancies', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await login(page, 'super_admin');
|
await login(page, 'super_admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('berths list - reservation-related affordances are clickable', async ({ page }) => {
|
test('berths list - tenancy-related affordances are clickable', async ({ page }) => {
|
||||||
await navigateTo(page, '/berths');
|
await navigateTo(page, '/berths');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ test.describe('exhaustive: berth reservations', () => {
|
|||||||
expect(result.errors, JSON.stringify(result.errors, null, 2)).toEqual([]);
|
expect(result.errors, JSON.stringify(result.errors, null, 2)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('berth detail - reservations tab opens and is interactive', async ({ page }) => {
|
test('berth detail - tenancies tab opens and is interactive', async ({ page }) => {
|
||||||
await navigateTo(page, '/berths');
|
await navigateTo(page, '/berths');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
@@ -29,9 +29,9 @@ test.describe('exhaustive: berth reservations', () => {
|
|||||||
await page.waitForURL(new RegExp(`/${PORT_SLUG}/berths/[^/]+`), { timeout: 10_000 });
|
await page.waitForURL(new RegExp(`/${PORT_SLUG}/berths/[^/]+`), { timeout: 10_000 });
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
const reservationsTab = page.getByRole('tab', { name: /reservations/i }).first();
|
const tenanciesTab = page.getByRole('tab', { name: /tenancies/i }).first();
|
||||||
if (await reservationsTab.isVisible({ timeout: 3000 }).catch(() => false)) {
|
if (await tenanciesTab.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||||
await reservationsTab.click();
|
await tenanciesTab.click();
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ test.describe('exhaustive: berth reservations', () => {
|
|||||||
expect(result.errors, JSON.stringify(result.errors, null, 2)).toEqual([]);
|
expect(result.errors, JSON.stringify(result.errors, null, 2)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reserve dialog opens and closes', async ({ page }) => {
|
test('create-tenancy dialog opens and closes', async ({ page }) => {
|
||||||
await navigateTo(page, '/berths');
|
await navigateTo(page, '/berths');
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
const firstRow = page.locator('tbody tr a, tbody tr button').first();
|
const firstRow = page.locator('tbody tr a, tbody tr button').first();
|
||||||
@@ -50,9 +50,9 @@ test.describe('exhaustive: berth reservations', () => {
|
|||||||
await firstRow.click();
|
await firstRow.click();
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
const reserveBtn = page.getByRole('button', { name: /reserve|new reservation/i }).first();
|
const createBtn = page.getByRole('button', { name: /create tenancy|reserve/i }).first();
|
||||||
if (await reserveBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
if (await createBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||||
await reserveBtn.click();
|
await createBtn.click();
|
||||||
const dialog = page.getByRole('dialog');
|
const dialog = page.getByRole('dialog');
|
||||||
await expect(dialog).toBeVisible({ timeout: 5000 });
|
await expect(dialog).toBeVisible({ timeout: 5000 });
|
||||||
const cancel = dialog.getByRole('button', { name: /cancel|close/i }).first();
|
const cancel = dialog.getByRole('button', { name: /cancel|close/i }).first();
|
||||||
@@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { clickEverythingOnPage } from '../../helpers/click-everything';
|
import { clickEverythingOnPage } from '../../helpers/click-everything';
|
||||||
import { login } from '../smoke/helpers';
|
import { login } from '../smoke/helpers';
|
||||||
|
|
||||||
const PORTAL_PAGES = ['/portal/yachts', '/portal/memberships', '/portal/my-reservations'];
|
const PORTAL_PAGES = ['/portal/yachts', '/portal/memberships', '/portal/my-tenancies'];
|
||||||
|
|
||||||
test.describe('exhaustive: client portal', () => {
|
test.describe('exhaustive: client portal', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createHandler as createReservationHandler,
|
createHandler as createTenancyHandler,
|
||||||
listHandler as listReservationsHandler,
|
listHandler as listTenanciesHandler,
|
||||||
} from '@/app/api/v1/berths/[id]/tenancies/handlers';
|
} from '@/app/api/v1/berths/[id]/tenancies/handlers';
|
||||||
import {
|
import {
|
||||||
getHandler as getReservationHandler,
|
getHandler as getTenancyHandler,
|
||||||
patchHandler as patchReservationHandler,
|
patchHandler as patchTenancyHandler,
|
||||||
deleteHandler as deleteReservationHandler,
|
deleteHandler as deleteTenancyHandler,
|
||||||
} from '@/app/api/v1/tenancies/[id]/handlers';
|
} from '@/app/api/v1/tenancies/[id]/handlers';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { berthTenancies } from '@/lib/db/schema/tenancies';
|
import { berthTenancies } from '@/lib/db/schema/tenancies';
|
||||||
@@ -53,7 +53,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
|
|||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await createReservationHandler(req, ctx, { id: berth.id });
|
const res = await createTenancyHandler(req, ctx, { id: berth.id });
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
const body = (await res.json()) as any;
|
const body = (await res.json()) as any;
|
||||||
expect(body.data.berthId).toBe(berth.id);
|
expect(body.data.berthId).toBe(berth.id);
|
||||||
@@ -81,7 +81,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
|
|||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await createReservationHandler(req, ctx, { id: berth.id });
|
const res = await createTenancyHandler(req, ctx, { id: berth.id });
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
|
|||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await createReservationHandler(req, ctxB, { id: berthA.id });
|
const res = await createTenancyHandler(req, ctxB, { id: berthA.id });
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
|
|||||||
startDate: new Date().toISOString(),
|
startDate: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await createReservationHandler(req, ctx, { id: urlBerth.id });
|
const res = await createTenancyHandler(req, ctx, { id: urlBerth.id });
|
||||||
expect(res.status).toBe(201);
|
expect(res.status).toBe(201);
|
||||||
const body = (await res.json()) as any;
|
const body = (await res.json()) as any;
|
||||||
expect(body.data.berthId).toBe(urlBerth.id);
|
expect(body.data.berthId).toBe(urlBerth.id);
|
||||||
@@ -153,7 +153,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
|
|||||||
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
||||||
|
|
||||||
// Create a reservation for berthA.
|
// Create a reservation for berthA.
|
||||||
await createReservationHandler(
|
await createTenancyHandler(
|
||||||
makeMockRequest('POST', `http://localhost/api/v1/berths/${berthA.id}/tenancies`, {
|
makeMockRequest('POST', `http://localhost/api/v1/berths/${berthA.id}/tenancies`, {
|
||||||
body: {
|
body: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -166,7 +166,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create a reservation for berthB.
|
// Create a reservation for berthB.
|
||||||
await createReservationHandler(
|
await createTenancyHandler(
|
||||||
makeMockRequest('POST', `http://localhost/api/v1/berths/${berthB.id}/tenancies`, {
|
makeMockRequest('POST', `http://localhost/api/v1/berths/${berthB.id}/tenancies`, {
|
||||||
body: {
|
body: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -178,7 +178,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
|
|||||||
{ id: berthB.id },
|
{ id: berthB.id },
|
||||||
);
|
);
|
||||||
|
|
||||||
const res = await listReservationsHandler(
|
const res = await listTenanciesHandler(
|
||||||
makeMockRequest('GET', `http://localhost/api/v1/berths/${berthA.id}/tenancies`),
|
makeMockRequest('GET', `http://localhost/api/v1/berths/${berthA.id}/tenancies`),
|
||||||
ctx,
|
ctx,
|
||||||
{ id: berthA.id },
|
{ id: berthA.id },
|
||||||
@@ -204,7 +204,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
|
|||||||
});
|
});
|
||||||
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
||||||
|
|
||||||
const createRes = await createReservationHandler(
|
const createRes = await createTenancyHandler(
|
||||||
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
||||||
body: {
|
body: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -217,7 +217,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
|
|||||||
);
|
);
|
||||||
const reservation = ((await createRes.json()) as any).data;
|
const reservation = ((await createRes.json()) as any).data;
|
||||||
|
|
||||||
const res = await getReservationHandler(
|
const res = await getTenancyHandler(
|
||||||
makeMockRequest('GET', `http://localhost/api/v1/tenancies/${reservation.id}`),
|
makeMockRequest('GET', `http://localhost/api/v1/tenancies/${reservation.id}`),
|
||||||
ctx,
|
ctx,
|
||||||
{ id: reservation.id },
|
{ id: reservation.id },
|
||||||
@@ -239,7 +239,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
|
|||||||
});
|
});
|
||||||
const ctxA = makeMockCtx({ portId: portA.id, permissions: makeFullPermissions() });
|
const ctxA = makeMockCtx({ portId: portA.id, permissions: makeFullPermissions() });
|
||||||
|
|
||||||
const createRes = await createReservationHandler(
|
const createRes = await createTenancyHandler(
|
||||||
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
||||||
body: {
|
body: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -253,7 +253,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
|
|||||||
const reservation = ((await createRes.json()) as any).data;
|
const reservation = ((await createRes.json()) as any).data;
|
||||||
|
|
||||||
const ctxB = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() });
|
const ctxB = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() });
|
||||||
const res = await getReservationHandler(
|
const res = await getTenancyHandler(
|
||||||
makeMockRequest('GET', `http://localhost/api/v1/tenancies/${reservation.id}`),
|
makeMockRequest('GET', `http://localhost/api/v1/tenancies/${reservation.id}`),
|
||||||
ctxB,
|
ctxB,
|
||||||
{ id: reservation.id },
|
{ id: reservation.id },
|
||||||
@@ -276,7 +276,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
});
|
});
|
||||||
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
||||||
|
|
||||||
const createRes = await createReservationHandler(
|
const createRes = await createTenancyHandler(
|
||||||
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
||||||
body: {
|
body: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -293,7 +293,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
|
|
||||||
it('activate: pending → active (200)', async () => {
|
it('activate: pending → active (200)', async () => {
|
||||||
const { ctx, reservation } = await seedReservation();
|
const { ctx, reservation } = await seedReservation();
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'activate' },
|
body: { action: 'activate' },
|
||||||
}),
|
}),
|
||||||
@@ -309,7 +309,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
const { ctx, reservation } = await seedReservation();
|
const { ctx, reservation } = await seedReservation();
|
||||||
|
|
||||||
// First activate.
|
// First activate.
|
||||||
await patchReservationHandler(
|
await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'activate' },
|
body: { action: 'activate' },
|
||||||
}),
|
}),
|
||||||
@@ -319,7 +319,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
|
|
||||||
// Then end.
|
// Then end.
|
||||||
const endDate = new Date('2027-01-01T00:00:00.000Z');
|
const endDate = new Date('2027-01-01T00:00:00.000Z');
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: {
|
body: {
|
||||||
action: 'end',
|
action: 'end',
|
||||||
@@ -338,7 +338,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
|
|
||||||
it('cancel: pending → cancelled (200)', async () => {
|
it('cancel: pending → cancelled (200)', async () => {
|
||||||
const { ctx, reservation } = await seedReservation();
|
const { ctx, reservation } = await seedReservation();
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'cancel', reason: 'client changed mind' },
|
body: { action: 'cancel', reason: 'client changed mind' },
|
||||||
}),
|
}),
|
||||||
@@ -354,7 +354,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
const { ctx, reservation } = await seedReservation();
|
const { ctx, reservation } = await seedReservation();
|
||||||
|
|
||||||
// pending → active.
|
// pending → active.
|
||||||
await patchReservationHandler(
|
await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'activate' },
|
body: { action: 'activate' },
|
||||||
}),
|
}),
|
||||||
@@ -362,7 +362,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
{ id: reservation.id },
|
{ id: reservation.id },
|
||||||
);
|
);
|
||||||
// active → ended.
|
// active → ended.
|
||||||
await patchReservationHandler(
|
await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: {
|
body: {
|
||||||
action: 'end',
|
action: 'end',
|
||||||
@@ -374,7 +374,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ended → activate should fail.
|
// ended → activate should fail.
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'activate' },
|
body: { action: 'activate' },
|
||||||
}),
|
}),
|
||||||
@@ -386,7 +386,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
|
|
||||||
it('returns 400 on invalid body shape (action missing)', async () => {
|
it('returns 400 on invalid body shape (action missing)', async () => {
|
||||||
const { ctx, reservation } = await seedReservation();
|
const { ctx, reservation } = await seedReservation();
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { notes: 'noop' },
|
body: { notes: 'noop' },
|
||||||
}),
|
}),
|
||||||
@@ -406,7 +406,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
tenancies: { view: true, manage: false, cancel: true },
|
tenancies: { view: true, manage: false, cancel: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'activate' },
|
body: { action: 'activate' },
|
||||||
}),
|
}),
|
||||||
@@ -423,7 +423,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
portId: port.id,
|
portId: port.id,
|
||||||
permissions: makeSalesAgentPermissions(),
|
permissions: makeSalesAgentPermissions(),
|
||||||
});
|
});
|
||||||
const res = await patchReservationHandler(
|
const res = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'cancel', reason: 'test' },
|
body: { action: 'cancel', reason: 'test' },
|
||||||
}),
|
}),
|
||||||
@@ -433,7 +433,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
|
|||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
|
|
||||||
// But activate succeeds with the same permissions set.
|
// But activate succeeds with the same permissions set.
|
||||||
const activateRes = await patchReservationHandler(
|
const activateRes = await patchTenancyHandler(
|
||||||
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
|
||||||
body: { action: 'activate' },
|
body: { action: 'activate' },
|
||||||
}),
|
}),
|
||||||
@@ -458,7 +458,7 @@ describe('DELETE /api/v1/tenancies/[id]', () => {
|
|||||||
});
|
});
|
||||||
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
|
||||||
|
|
||||||
const createRes = await createReservationHandler(
|
const createRes = await createTenancyHandler(
|
||||||
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
|
||||||
body: {
|
body: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -471,7 +471,7 @@ describe('DELETE /api/v1/tenancies/[id]', () => {
|
|||||||
);
|
);
|
||||||
const reservation = ((await createRes.json()) as any).data;
|
const reservation = ((await createRes.json()) as any).data;
|
||||||
|
|
||||||
const delRes = await deleteReservationHandler(
|
const delRes = await deleteTenancyHandler(
|
||||||
makeMockRequest('DELETE', `http://localhost/api/v1/tenancies/${reservation.id}`),
|
makeMockRequest('DELETE', `http://localhost/api/v1/tenancies/${reservation.id}`),
|
||||||
ctx,
|
ctx,
|
||||||
{ id: reservation.id },
|
{ id: reservation.id },
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ describe('berth-tenancies.service - lifecycle transitions', () => {
|
|||||||
// ─── listTenancies ────────────────────────────────────────────────────────
|
// ─── listTenancies ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('berth-tenancies.service - listTenancies', () => {
|
describe('berth-tenancies.service - listTenancies', () => {
|
||||||
async function makeReservation(portId: string, opts?: { berthId?: string }) {
|
async function makeTenancyLocal(portId: string, opts?: { berthId?: string }) {
|
||||||
const berth = opts?.berthId ? { id: opts.berthId } : await makeBerth({ portId });
|
const berth = opts?.berthId ? { id: opts.berthId } : await makeBerth({ portId });
|
||||||
const client = await makeClient({ portId });
|
const client = await makeClient({ portId });
|
||||||
const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: client.id });
|
const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: client.id });
|
||||||
@@ -351,8 +351,8 @@ describe('berth-tenancies.service - listTenancies', () => {
|
|||||||
it('is tenant-scoped', async () => {
|
it('is tenant-scoped', async () => {
|
||||||
const portA = await makePort();
|
const portA = await makePort();
|
||||||
const portB = await makePort();
|
const portB = await makePort();
|
||||||
const resA = await makeReservation(portA.id);
|
const resA = await makeTenancyLocal(portA.id);
|
||||||
await makeReservation(portB.id);
|
await makeTenancyLocal(portB.id);
|
||||||
|
|
||||||
const result = await listTenancies(portA.id, {
|
const result = await listTenancies(portA.id, {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -367,8 +367,8 @@ describe('berth-tenancies.service - listTenancies', () => {
|
|||||||
|
|
||||||
it('filters by status', async () => {
|
it('filters by status', async () => {
|
||||||
const port = await makePort();
|
const port = await makePort();
|
||||||
const resPending = await makeReservation(port.id);
|
const resPending = await makeTenancyLocal(port.id);
|
||||||
const resActive = await makeReservation(port.id);
|
const resActive = await makeTenancyLocal(port.id);
|
||||||
await activate(resActive.id, port.id, {}, makeAuditMeta({ portId: port.id }));
|
await activate(resActive.id, port.id, {}, makeAuditMeta({ portId: port.id }));
|
||||||
|
|
||||||
const activeList = await listTenancies(port.id, {
|
const activeList = await listTenancies(port.id, {
|
||||||
|
|||||||
Reference in New Issue
Block a user