Fresh-eyes review round with 6 independent agents (money-safety, concurrency, Square wire parity, security, frontend flow, testing-gaps). Every finding was independently verified against the code before fixing. All backend changes now carry full test suites (10+ new tests, each verified to FAIL without its guard). All 20 packages green, race detector clean. Money-safety: - Gift-card purchase refunds no longer create money: manual refunds of a no-booking (gift-card purchase) payment are rejected with a clear message in the direct handler AND never re-issued by the sweep-resume path (processManualPaymentGroup skips them; reconcile-then-fail, no re-issue). - BuyGiftCard no-client-key fallback: derived deterministically under the advisory lock (pending-row reuse fixes lost-response double-charge; completed-row sequence advance preserves distinct-purchase collapse fix). - Terminal completion is never unrecorded: activeTerminalCheckoutID now calls recordUntrackedTerminalPayment when a provisional (tmp-) checkout is found COMPLETED at Square (previously only marked the row COMPLETED — a lost poll left the payment invisible and unrefundable). - Sweep: provisional tmp- checkout rows are resolved against Square first (COMPLETED → record; live → keep guard; NOT_FOUND/CANCELED → fail; ambiguous → leave pending) instead of blind-failing a possibly-live checkout. recordUntrackedTerminalPayment re-checks the booking status (FOR UPDATE) and refuses to record on a cancelled booking, inserting a critical_payment_log admin notification instead. Till-sale post-charge UPDATE now requires status='pending' (no resurrection of a clawed-back sale). Frontend (Svelte 5): - UserPaymentModal keeps CardSelection mounted through processing (bind:this ref + Square iframe survive the loyalty/tokenize awaits) — new-card payments work again. - BookingFlow clears the cached nonce/verification pair on any failure (retry re-tokenizes fresh; idempotency key retained for dedup); 409 'already paid' refetches the booking and reconciles depositPaid so the confirmation gate opens; Back button disabled during processing. - Synchronous double-submit guards on buyGiftCard/redeemGiftCard/submitTip. Square wire parity (mock vs real): - processing_fee sign unified (negated at paymentFromSquare; mock agrees). - SimulateSourceUsed (SOURCE_USED, 400) matches real CreateCard. - GetCardsOnFile excludes disabled cards (matches ListCards). - ForcePaymentStatus toggle + tests prove the charge path can't be status-blind. - CreateCheckout rejects empty device_id (env fallback SQUARE_TERMINAL_DEVICE_ID); completed terminal checkout's payment resolvable by id. Security: - 2FA attempt-map data race fixed: lastAt is atomic.Int64 (nanos) — eviction scan reads race-free; concurrent verify+evict tests under -race. - Backend refuses to start on weak/placeholder JWT_SECRET_KEY (<32 chars or known public placeholders) with openssl rand -hex 32 guidance. - Dockerfile no longer COPYs .env (secrets injected via compose env_file). - SabreDAV requires DAV_ADMIN_PASSWORD (no admin/admin default); compose fails at config time when missing. Testing gaps closed (each verified to FAIL without its guard): - refunded-dedup 409 (CreateBookingPayment), keyed sweep past-retention blind-fail, reconcile status-switch (CANCELED/FAILED/APPROVED/PENDING/unknown in both by-key and by-id paths), resolveChargeSource Square-failure branches, structured 500 / CARD_DECLINED / cancelled-context E2E (row stays pending), deriveBookingPaymentIdempotencyKey >45-char truncation, webhook findPaymentByDisputeID fallback, clawbackOneTillSale non-gift-card branch, dispute.evidence / terminal.checkout dispatch. Infra: - local-dev-2.sh fails loudly on port-5432 squatters / docker compose failures (previously died silently under ERR_EXIT with hidden output). - Test harness defaults SQUARE_TERMINAL_DEVICE_ID; money_safety_fixes_test.go gained the missing build tag. Verification: go test -tags test,dev -count=1 -parallel 8 ./... (20/20 ok), -race clean on 2FA + payments money paths, go build ./... + -tags dev, go vet clean, svelte-check 0 errors, env-docs gate OK (36 vars), docker compose config valid.
101 lines
2.6 KiB
YAML
101 lines
2.6 KiB
YAML
services:
|
|
postgres:
|
|
image: postgres:17
|
|
container_name: postgres
|
|
restart: always
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
POSTGRES_DB: ${POSTGRES_DB}
|
|
ports:
|
|
- "5432:5432" # locally, do not push to prod
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
- ./init-scripts/init-script.sql:/docker-entrypoint-initdb.d/init-script.sql:ro
|
|
networks:
|
|
- appnet
|
|
|
|
backend:
|
|
build:
|
|
context: ./backend
|
|
dockerfile: Dockerfile
|
|
container_name: backend
|
|
restart: always
|
|
env_file:
|
|
- ./.env # created via `cp .env.example .env` (see README Getting Started)
|
|
volumes:
|
|
- ./backend/bin:/app/bin # Mount your compiled binary
|
|
depends_on:
|
|
- postgres
|
|
networks:
|
|
- appnet
|
|
|
|
sabredav:
|
|
image: php:8.2-fpm
|
|
container_name: sabredav
|
|
restart: always
|
|
working_dir: /var/www/dav
|
|
environment:
|
|
POSTGRES_HOST: postgres
|
|
POSTGRES_PORT: 5432
|
|
POSTGRES_DB: ${POSTGRES_DB}
|
|
POSTGRES_USER: ${POSTGRES_USER}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
DAV_ADMIN_PASSWORD: ${DAV_ADMIN_PASSWORD:?DAV_ADMIN_PASSWORD must be set — generate a strong random value with `openssl rand -hex 32`}
|
|
volumes:
|
|
- ./sabredav:/var/www/dav
|
|
depends_on:
|
|
- postgres
|
|
networks:
|
|
- appnet
|
|
command: >
|
|
bash -c " apt-get update && apt-get install -y git unzip libzip-dev libpq-dev && docker-php-ext-install pdo pdo_pgsql zip && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && if [ ! -f /var/www/dav/vendor/autoload.php ]; then
|
|
cd /var/www/dav && composer install --no-dev --optimize-autoloader;
|
|
fi && php-fpm "
|
|
|
|
nginx:
|
|
image: nginx:stable
|
|
container_name: nginx
|
|
restart: always
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "9000:9000" # Rustfs S3 API
|
|
volumes:
|
|
- ./nginx/conf.d:/etc/nginx/conf.d
|
|
- ./nginx/certs:/etc/nginx/certs
|
|
- ./frontend/build:/usr/share/nginx/html # Serve frontend
|
|
- ./sabredav:/var/www/dav
|
|
- rustfs_data:/data
|
|
depends_on:
|
|
- backend
|
|
- sabredav
|
|
networks:
|
|
- appnet
|
|
|
|
rustfs:
|
|
image: rustfs/rustfs:latest
|
|
container_name: rustfs
|
|
restart: always
|
|
ports:
|
|
- "9000:9000"
|
|
- "9001:9001"
|
|
environment:
|
|
- RUSTFS_ADMIN_ACCESS_KEY=rustfsadmin
|
|
- RUSTFS_ADMIN_SECRET_KEY=rustfsadmin
|
|
- RUSTFS_ACCESS_KEY=rustfsadmin
|
|
- RUSTFS_SECRET_KEY=rustfsadmin
|
|
- RUSTFS_BUCKET=crussell
|
|
volumes:
|
|
- rustfs_data:/data
|
|
networks:
|
|
- appnet
|
|
|
|
volumes:
|
|
pgdata:
|
|
rustfs_data:
|
|
|
|
|
|
networks:
|
|
appnet:
|