Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
d71dfe7
docs: design request-first direct bookings
agustinjch Aug 24, 2026
0a6cdff
docs: plan booking request implementation
agustinjch Aug 24, 2026
9e2e7a4
feat(database): add booking request persistence
agustinjch Aug 24, 2026
5453ccb
feat(booking-requests): define lifecycle and money rules
agustinjch Aug 24, 2026
76fbc57
fix(booking-requests): tighten denial and allocation rules
agustinjch Aug 24, 2026
667e7ea
fix(booking-requests): reject negative movement balances
agustinjch Aug 24, 2026
57402d3
feat(booking-engine): configure request forms
agustinjch Aug 24, 2026
7e65e4c
fix(booking-engine): tighten request form settings
agustinjch Aug 24, 2026
20dd540
fix(booking-engine): lock request config updates
agustinjch Aug 24, 2026
53da165
feat(payments): save and charge request payment methods
agustinjch Aug 24, 2026
23d08c6
fix(payments): harden saved payment method gateway
agustinjch Aug 24, 2026
2c16ed4
feat(booking-requests): accept public submissions
agustinjch Aug 24, 2026
7c1c024
fix(booking-requests): harden public submission
agustinjch Aug 24, 2026
bc2b573
fix(booking-requests): persist created consequence
agustinjch Aug 24, 2026
5254ccc
fix(booking-requests): stabilize webhook event id
agustinjch Aug 24, 2026
23d657a
feat(booking-requests): review and convert requests
agustinjch Aug 24, 2026
1ac9f00
fix(booking-requests): harden acceptance decisions
agustinjch Aug 24, 2026
f729e33
fix(booking-requests): preserve accepted pricing integrity
agustinjch Aug 24, 2026
d61d0b1
fix(booking-requests): suppress replayed service effects
agustinjch Aug 24, 2026
f19dc12
fix(booking-requests): recover once-service posting state
agustinjch Aug 24, 2026
50b3467
feat(booking-requests): manage partial payments
agustinjch Aug 24, 2026
1da9721
fix(booking-requests): harden payment recovery
agustinjch Aug 24, 2026
ad5497d
fix(booking-requests): finalize financial recovery
agustinjch Aug 24, 2026
dd02580
fix(booking-requests): close payment correlation gaps
agustinjch Aug 24, 2026
ec4ce69
fix(booking-requests): harden payment recovery identity
agustinjch Aug 24, 2026
4c97c42
fix(booking-requests): audit financial recovery repairs
agustinjch Aug 24, 2026
f7ceeaf
fix(booking-requests): preserve financial audit history
agustinjch Aug 24, 2026
30aff70
feat(booking-requests): send audited request emails
agustinjch Aug 24, 2026
1289cf2
fix(booking-requests): harden email delivery recovery
agustinjch Aug 24, 2026
6bea578
fix(booking-requests): serialize email retry delivery
agustinjch Aug 24, 2026
3d5b080
fix(booking-requests): hard-close timed out SMTP sends
agustinjch Aug 24, 2026
fc31585
feat(booking-widget): submit booking requests
agustinjch Aug 25, 2026
3e7ab65
fix(booking-widget): harden booking request flow
agustinjch Aug 25, 2026
62cffc5
fix(booking-widget): finalize request flow review
agustinjch Aug 25, 2026
236ba4e
fix(booking-widget): reset completed request on home
agustinjch Aug 25, 2026
1997569
feat(dashboard): configure booking request forms
agustinjch Aug 25, 2026
92fae86
fix(booking-engine): harden request settings editor
agustinjch Aug 25, 2026
c88d70a
fix(booking-engine): preserve rolling settings compatibility
agustinjch Aug 25, 2026
61c6b78
fix(booking-request): align locked config questions
agustinjch Aug 25, 2026
0c2110b
feat(dashboard): manage booking requests
agustinjch Aug 25, 2026
1c51d7c
fix(booking-requests): harden staff operations
agustinjch Aug 25, 2026
86c50b1
fix(booking-requests): close remaining staff review gaps
agustinjch Aug 25, 2026
9e75e3d
fix(booking-requests): preserve canonical staff state
agustinjch Aug 25, 2026
d813d3c
fix(booking-requests): preserve precise audit timelines
agustinjch Aug 25, 2026
4fcedd4
feat(booking-requests): amend accepted stays
agustinjch Aug 25, 2026
226a7e6
fix(booking-requests): harden accepted stay amendments
agustinjch Aug 25, 2026
aec5c66
fix(booking-requests): serialize accepted pricing ledger
agustinjch Aug 25, 2026
b7dc5a5
fix(booking-requests): harden accepted pricing operations
agustinjch Aug 25, 2026
a6c76b7
fix(booking-requests): close accepted stay review gaps
agustinjch Aug 25, 2026
da34f4e
fix(booking-requests): finalize accepted stay races
agustinjch Aug 25, 2026
93490ee
test(booking-requests): verify the complete workflow
agustinjch Aug 25, 2026
5f28a48
test(booking-requests): harden vertical verification
agustinjch Aug 25, 2026
d7be304
fix(booking-requests): preserve instant deposit flow
agustinjch Aug 25, 2026
057206c
fix(booking-requests): support local and Stripe card setup
agustinjch Aug 25, 2026
821c081
Merge remote-tracking branch 'upstream/main' into feat/booking-requests
agustinjch Aug 25, 2026
752b17a
docs(booking-requests): normalize markdown whitespace
agustinjch Aug 25, 2026
870042f
fix(booking-requests): harden card setup and migrations
agustinjch Aug 25, 2026
ff444c3
fix(payments): restore instant Stripe refund reconciliation
agustinjch Aug 26, 2026
b04a021
fix(payments): ignore unrelated Stripe account events
agustinjch Aug 26, 2026
7defd18
fix(payments): classify malformed Stripe metadata
agustinjch Aug 26, 2026
e446382
fix(payments): ignore linked legacy Stripe refunds
agustinjch Aug 26, 2026
fd8211d
fix(booking-requests): enforce required card policy
agustinjch Aug 26, 2026
c11afdb
fix(booking-requests): edit unallocated installment balances
agustinjch Aug 26, 2026
983dab7
fix(booking-requests): honor durable installment allocations
agustinjch Aug 26, 2026
f76a248
fix(booking-requests): validate shared payment cache
agustinjch Aug 26, 2026
8aae820
fix(booking-requests): validate ledger currency at submission
agustinjch Aug 26, 2026
1f9fe4f
fix(booking-requests): enforce ISO currency precision
agustinjch Aug 26, 2026
ee4872c
fix(booking-engine): audit request configuration changes
agustinjch Aug 26, 2026
9d1c0c3
fix(booking-engine): harden config audit snapshots
agustinjch Aug 26, 2026
0fd4b73
fix(booking-engine): close audit serialization gaps
agustinjch Aug 26, 2026
a144613
refactor(database): remove booking request runtime SQL
agustinjch Aug 26, 2026
1c201eb
fix(database): make remediation migration concurrency safe
agustinjch Aug 26, 2026
7ba43f2
test(database): cover migration sequence reservation race
agustinjch Aug 26, 2026
92498ac
test(database): cover sequence equality replay
agustinjch Aug 26, 2026
65aa501
test(booking-requests): protect the default booking flow
agustinjch Aug 26, 2026
60c8a85
test(booking-requests): harden default flow gate
agustinjch Aug 26, 2026
62e4ae0
test(booking-requests): secure release gate provisioning
agustinjch Aug 26, 2026
3d0fc53
test(booking-requests): redact provisioning diagnostics
agustinjch Aug 26, 2026
610a6a1
test(booking-requests): cover conninfo escapes
agustinjch Aug 26, 2026
564e4a5
test(booking-requests): cover multiline conninfo escapes
agustinjch Aug 26, 2026
cb5be8a
fix: close final booking request review gaps
agustinjch Aug 26, 2026
dd37a1d
fix: reject duplicate booking service selections
agustinjch Aug 26, 2026
e25694b
fix: reject unlinked owned Stripe charges
agustinjch Aug 26, 2026
85c57c3
fix: allow exact refund replay after denial
agustinjch Aug 26, 2026
3d4d0f3
fix: rotate discarded card setup attempts
agustinjch Aug 26, 2026
63eefbd
fix: preserve local financial timestamps
agustinjch Aug 26, 2026
e52ebf5
test: keep request setup e2e provenance explicit
agustinjch Aug 26, 2026
084380a
docs: sync full workspace test counts
agustinjch Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Domain glossary

## Booking Request

A guest's request for the property to review a sellable stay before deciding
whether to create a reservation. A Booking Request does not reserve inventory.

## Request Mode

A direct-booking mode in which submitting the guest form creates a Booking
Request instead of a reservation.

## Waitlist Entry

Non-deducting demand recorded when the requested stay is not currently
available. A Waitlist Entry is not a Booking Request.

## Quote Snapshot

The immutable record of the offer shown when a Booking Request was submitted.
Later prices and accepted prices do not rewrite this record.

## Accepted Price

The stay price chosen by staff when accepting a Booking Request. It can match
the Quote Snapshot, the current authoritative quote, or a justified custom
price.

## Payment Plan

A staff-managed set of expected partial payments. A Payment Plan can express
amounts or percentages and due milestones, but never initiates a payment by
itself.

## Payment Movement

An attempted or completed movement of money, either through the configured
card gateway or recorded after taking place outside HAIP.

## Stay Amendment

An audited change to an accepted reservation, such as extending its departure
date. A Stay Amendment does not rewrite the original Booking Request or its
Accepted Price.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<img src="https://img.shields.io/badge/NestJS-framework-E0234E?logo=nestjs&logoColor=white" alt="NestJS" />
<img src="https://img.shields.io/badge/PostgreSQL-database-4169E1?logo=postgresql&logoColor=white" alt="PostgreSQL" />
<img src="https://img.shields.io/badge/License-Apache%202.0-blue" alt="Apache 2.0 License" />
<img src="https://img.shields.io/badge/Tests-1568%20passing-brightgreen" alt="1568 Tests Passing" /> <img src="https://img.shields.io/badge/AI%20Agents-12%20built--in-blueviolet" alt="12 AI Agents" />
<img src="https://img.shields.io/badge/Tests-2235%20passing-brightgreen" alt="2235 Tests Passing" /> <img src="https://img.shields.io/badge/AI%20Agents-12%20built--in-blueviolet" alt="12 AI Agents" />
</p>

<p align="center">
Expand Down Expand Up @@ -510,7 +510,7 @@ Operator notes for activating existing adapters, metasearch landings on the dire
| OTA Channels | Booking.com + Expedia (EQC) + SiteMinder + DerbySoft | Direct + aggregated OTA connectivity (ARI + content) |
| XML Processing | fast-xml-parser | Booking.com OTA XML protocol |
| Package Manager | pnpm workspaces | Monorepo management |
| Testing | Vitest (1568 tests across 218 test files) | Unit and integration tests || Build | tsup (packages) + Vite (dashboard) + nest build (API) | Fast builds |
| Testing | Vitest (2235 passing tests across 259 files with passing tests) | Unit and integration tests || Build | tsup (packages) + Vite (dashboard) + nest build (API) | Fast builds |
| Containers | Docker + docker-compose | Local dev and production deployment |
| CI/CD | GitHub Actions | Automated testing, builds, and releases |

Expand Down Expand Up @@ -642,7 +642,7 @@ Before going live, verify the items in [`docs/deployment.md`](./docs/deployment.
### Run tests

```bash
# All tests (1568 tests across 218 test files)
# Passing-test count: 2235 test cases across 259 files (skipped excluded)

# API tests only
pnpm --filter @telivityhaip/api test
Expand Down Expand Up @@ -1190,7 +1190,7 @@ HAIP is built in public and contributions are welcome.
pnpm install # Install dependencies
pnpm build # Build all workspace packages
pnpm dev # Start API in dev mode (hot reload)
pnpm test # Run all tests (1568 tests, 218 files)
pnpm test # Run all tests (2235 passing, 259 files with passes; skipped excluded)
pnpm lint # ESLint
```

Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"fast-xml-parser": "^5.5.10",
"jsonwebtoken": "^9.0.3",
"jwks-rsa": "^4.0.1",
"nodemailer": "^9.0.5",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"postgres": "^3.4.5",
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { PolicyModule } from './modules/policy/policy.module';
import { GroupsModule } from './modules/groups/groups.module';
import { AdminModule } from './modules/admin/admin.module';
import { BookingEngineModule } from './modules/booking-engine/booking-engine.module';
import { BookingRequestModule } from './modules/booking-request/booking-request.module';
import { ImportModule } from './modules/import/import.module';
import { MigrationModule } from './modules/migration/migration.module';
import { AccountingExportModule } from './modules/accounting-export/accounting-export.module';
Expand All @@ -59,7 +60,7 @@ const imports: any[] = [
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
EventEmitterModule.forRoot(),
EventEmitterModule.forRoot({ wildcard: true }),
DatabaseModule,
HealthModule,
PropertyModule,
Expand Down Expand Up @@ -90,6 +91,7 @@ const imports: any[] = [
GroupsModule,
AdminModule,
BookingEngineModule,
BookingRequestModule,
ImportModule,
MigrationModule,
AccountingExportModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { AcceptedPricingSnapshot } from '@telivityhaip/database';

export type AcceptedReservationServiceCandidate = {
id: string;
serviceId: string;
status?: string | null;
sourceChannel?: string | null;
createdAt?: Date | string | null;
};

function createdAtValue(value: Date | string | null | undefined): number {
if (value instanceof Date) return value.getTime();
if (typeof value === 'string') {
const parsed = Date.parse(value);
if (Number.isFinite(parsed)) return parsed;
}
return Number.MAX_SAFE_INTEGER;
}

/**
* Match each snapshot service to exactly one operational row. Booking Request
* acceptance creates `booking_engine` rows; later front-desk duplicates are
* legal extras and must never duplicate or resurrect that accepted component.
* The time/id fallback makes legacy rows deterministic when provenance is absent.
*/
export function matchAcceptedReservationServiceRows<
T extends AcceptedReservationServiceCandidate,
>(
pricing: Pick<AcceptedPricingSnapshot, 'services'> | null | undefined,
rows: readonly T[],
): Map<string, T> {
const matched = new Map<string, T>();
if (!pricing) return matched;

for (const service of pricing.services) {
const candidates = rows
.filter((row) => row.serviceId === service.serviceId)
.sort((left, right) => {
const provenance = Number(right.sourceChannel === 'booking_engine')
- Number(left.sourceChannel === 'booking_engine');
if (provenance !== 0) return provenance;
const created = createdAtValue(left.createdAt) - createdAtValue(right.createdAt);
return created !== 0 ? created : left.id.localeCompare(right.id);
});
if (candidates[0]) matched.set(service.serviceId, candidates[0]);
}

return matched;
}
13 changes: 13 additions & 0 deletions apps/api/src/common/crypto/confirmation-number.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it, vi } from 'vitest';
import { generateConfirmationNumber } from './confirmation-number';

describe('generateConfirmationNumber', () => {
it('uses a 128-bit cryptographic entropy seam and a non-enumerable shape', () => {
const entropy = vi.fn((bytes: number) => Buffer.alloc(bytes, 0xa5));

const confirmation = generateConfirmationNumber(entropy);

expect(entropy).toHaveBeenCalledWith(16);
expect(confirmation).toMatch(/^HAIP-[0-9A-HJKMNP-TV-Z]{32}$/);
});
});
21 changes: 21 additions & 0 deletions apps/api/src/common/crypto/confirmation-number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { randomBytes } from 'node:crypto';

const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

export type ConfirmationEntropy = (bytes: number) => Uint8Array;

/** A guest-facing bearer credential backed by exactly 128 bits of entropy. */
export function generateConfirmationNumber(
entropy: ConfirmationEntropy = randomBytes,
): string {
const bytes = entropy(16);
if (bytes.length !== 16) {
throw new Error('Confirmation entropy source must return 16 bytes');
}
let token = '';
for (const byte of bytes) {
token += CROCKFORD[byte & 0x1f];
token += CROCKFORD[(byte >> 5) & 0x1f];
}
return `HAIP-${token}`;
}
Loading