Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
124d978
docs: replace default Next.js README with custom Stackwise company pr…
daniel-teddy Aug 7, 2026
da72d3d
feat: implement FAQ section, landing page components, and site naviga…
daniel-teddy Aug 20, 2026
003de42
chore: bump version from 0.1.0 to 0.2.0 in package.json
daniel-teddy Aug 20, 2026
fc31725
feat: add bilingual EN/FR site copy for SuperCmd-style sections
daniel-teddy Aug 20, 2026
731e6e9
feat: add high-fidelity landing UI primitives
daniel-teddy Aug 20, 2026
6a871ed
feat: rebuild landing page with full SuperCmd-like section set
daniel-teddy Aug 20, 2026
e2bcefb
feat: add /en and /fr locale routing with language switcher
daniel-teddy Aug 20, 2026
caf8346
feat: enhance deployment process and configuration for Cloudflare Wor…
daniel-teddy Aug 20, 2026
738d119
feat: enhance landing page with locale support and new visual components
daniel-teddy Aug 20, 2026
141abfc
feat: enhance hero and landing components with new features and styling
daniel-teddy Aug 20, 2026
967ed2f
refactor: simplify SiteNav component and update brand name in content
daniel-teddy Aug 20, 2026
f0457de
fix: update deployment message format and improve argument handling i…
daniel-teddy Aug 20, 2026
27ecb19
refactor: update language labels in LanguageSwitcher and comment out …
daniel-teddy Aug 22, 2026
7fb636e
refactor: update booking labels in English and French site copy
daniel-teddy Aug 22, 2026
6031602
refactor: update branding and improve copy in English and French content
daniel-teddy Aug 22, 2026
2b749c9
refactor: remove unused image assets and update layout metadata for SEO
daniel-teddy Aug 22, 2026
6f38e37
feat: add Open Graph images for improved social media sharing
daniel-teddy Aug 22, 2026
82bb842
refactor: update image paths for Open Graph metadata
daniel-teddy Aug 22, 2026
0e6b0f5
refactor: correct site name casing in metadata
daniel-teddy Aug 22, 2026
8ced8b2
refactor: update Open Graph image URLs to use absolute paths
daniel-teddy Aug 22, 2026
fd971ba
refactor: simplify button className logic in ProviderTabs component
daniel-teddy Sep 10, 2026
2977477
refactor: clean up commented code and improve structure in ProviderTa…
daniel-teddy Sep 10, 2026
1e558cf
Implement code changes to enhance functionality and improve performance
daniel-teddy Sep 10, 2026
d988a39
feat: add new tools to ProviderTabs component
daniel-teddy Sep 10, 2026
2fd27fa
feat: implement admin dashboard and payment link management features
daniel-teddy Sep 22, 2026
92c5390
feat: implement secret management for KPay integration
daniel-teddy Sep 22, 2026
409cead
feat: implement withdrawal functionality for KPay integration
daniel-teddy Sep 22, 2026
1af7a0a
feat: enhance migration script and update payment link builder
daniel-teddy Sep 22, 2026
852c538
feat: add schema management and enhance database readiness checks
daniel-teddy Sep 22, 2026
cba642d
refactor: remove customer name and email from payment initiation meta…
daniel-teddy Sep 22, 2026
e5df3e4
feat: enhance invoice components with compact mode and styling improv…
daniel-teddy Sep 22, 2026
b1cfd2b
refactor: simplify InvoiceHeader and update payment button text
daniel-teddy Sep 22, 2026
09f6076
feat: enhance payment methods and update service catalog pricing
daniel-teddy Sep 22, 2026
9617ee1
feat: enhance payment link management and environment handling
daniel-teddy Sep 22, 2026
784dd8c
feat: add db:reset:prod-payments script and enhance payment link mana…
daniel-teddy Sep 22, 2026
83fe2a0
feat: enhance admin dashboard and payment link management
daniel-teddy Sep 22, 2026
cb1d658
feat: enhance admin dashboard with payment link filtering and totals …
daniel-teddy Sep 22, 2026
b5bc649
refactor: streamline database schema management and email template ha…
daniel-teddy Sep 22, 2026
f1a2ddf
feat: enhance admin layout with metadata and favicon support
daniel-teddy Sep 22, 2026
54b6c0c
feat: update component styles and layout for improved UI consistency
daniel-teddy Sep 22, 2026
fdb5f0c
fix: adjust spacing and padding in InvoiceView for improved layout co…
daniel-teddy Sep 22, 2026
d667c85
fix: update contact prompt in InvoiceView for improved clarity
daniel-teddy Sep 22, 2026
65bbc5a
fix: update date formatting in InvoiceView for improved clarity
daniel-teddy Sep 22, 2026
3ba10ae
docs: update README with payment flow and legal compliance information
daniel-teddy Sep 22, 2026
d34c153
feat: implement payment legal notice and receipt access control
daniel-teddy Sep 22, 2026
f05c70a
feat: update invoice number handling and database schema
daniel-teddy Sep 22, 2026
a07be94
refactor: remove invoice number UUID migration and update generation …
daniel-teddy Sep 22, 2026
6b0db9a
feat: add pay portal styles and update layout for light theme
daniel-teddy Sep 22, 2026
14dab04
feat: implement payment checkout and receipt handling components
daniel-teddy Sep 22, 2026
9d2188d
feat: implement payment audit logging and admin display
daniel-teddy Sep 22, 2026
3f5356b
feat: enhance payment audit logging and admin interface
daniel-teddy Sep 22, 2026
a9a9194
refactor: update payment audit log components and enhance filtering
daniel-teddy Sep 22, 2026
6d62028
feat: add allowed payment methods to payment links
daniel-teddy Sep 22, 2026
cf8bbee
feat: integrate payment methods into payment link details
daniel-teddy Sep 22, 2026
10c717e
feat: enhance payment initiation and error handling
daniel-teddy Sep 23, 2026
a1071ef
refactor: enhance date formatting and update components
daniel-teddy Sep 23, 2026
3f2285f
feat: add manual payment recording and receipt management
daniel-teddy Sep 23, 2026
d6ff44e
feat: implement pagination for payment audit logs and links
daniel-teddy Sep 23, 2026
a72c684
refactor: reorganize payment-related imports and move functions to ne…
daniel-teddy Sep 23, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
/out/
/.open-next/
/.wrangler/
.wrangler-secrets.tmp.json

# production
/build
Expand Down
78 changes: 73 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,94 @@ Locales:
| `npm run lint` | ESLint |
| `npm test` | Unit tests (Vitest) |
| `npm run typecheck` | TypeScript (`tsc --noEmit`) |
| `npm run db:migrate` | Apply SQL migrations to Neon (uses `.env.local` → `.env.prod` → `.env`) |
| `npm run db:migrate:prod` | Apply migrations to **production** Neon (`MIGRATE_ENV=prod`, reads `.env.prod` only) |
| `npm run db:reset:prod-payments` | Dry-run cleanup of sandbox rows in prod Neon; pass `--confirm` to apply |
| `npm run preview` | Build OpenNext worker and preview in `workerd` via Wrangler |
| `npm run build:worker` | Build the Cloudflare Worker bundle only |
| `npm run deploy` | Build + deploy to Cloudflare Workers |
| `npm run deploy:live` | Build + deploy current branch/commit as the **live** production Worker (tagged) |
| `npm run deploy:live` | Full production deploy — see [Deploy to production](#deploy-to-production) |
| `npm run cf:secrets` | Upload Worker secrets from `.env.prod` without redeploying |
| `npm run cf:whoami` | Show Cloudflare auth account |
| `npm run cf:status` | Show the live Workers deployment status |

### Deploy from your machine
### Environment files

| File | Purpose |
| --- | --- |
| `.env.local` | Local development (gitignored). Copy from `.env.example`. |
| `.env.prod` | Production secrets and config (gitignored). Used by deploy and prod migrations. |
| `.env.example` | Template with placeholder keys — safe to commit. |

Never commit `.env.local` or `.env.prod`.

### Database migrations

Migrations live in `migrations/*.sql` and are applied in filename order.

```bash
# Local / dev Neon
npm run db:migrate

# Production Neon (uses DATABASE_URL from .env.prod)
npm run db:migrate:prod
```

The app also runs `ensureSchema()` on first DB access, which applies any pending migration files automatically.

### Deploy to production

You do not need GitHub Actions or the Cloudflare dashboard to ship:

```bash
# one-time: log in if needed
# One-time: log in to Cloudflare
npx wrangler login

# ship the current branch as production
# Create .env.prod with production values (see .env.example)
# Then ship:
npm run deploy:live
```

`deploy:live` tags the Worker version with the current git branch and short SHA (and marks `-dirty` if you have uncommitted changes), then rolls it out to 100% production traffic.
#### What `npm run deploy:live` does

Runs `scripts/deploy-live.sh`, which:

1. **Checks auth** — exits if `wrangler whoami` fails.
2. **Build env** — copies `.env.prod` → `.env.production.local` so `NEXT_PUBLIC_*` (e.g. site URL) is baked into the Next.js build, not localhost from `.env.local`.
3. **Sync secrets** — calls `scripts/sync-cloudflare-env.mjs` to upload every key in `.env.prod` to the `stackwise-technologies` Worker via `wrangler secret bulk`.
4. **Build** — runs `npm run build:worker` (OpenNext + Next.js production build).
5. **Deploy** — publishes to Cloudflare Workers at 100% traffic, tagged with git branch + short SHA (e.g. `main@abc1234`).

#### Sync secrets only (no redeploy)

After editing `.env.prod`, push new secrets without rebuilding:

```bash
npm run cf:secrets
# equivalent to: node scripts/sync-cloudflare-env.mjs .env.prod
```

Requires `wrangler` login. Uploads all non-comment `KEY=value` lines from the file as encrypted Worker secrets.

#### Script reference (`scripts/`)

| File | Invoked by | Description |
| --- | --- | --- |
| `scripts/deploy-live.sh` | `npm run deploy:live` | End-to-end production deploy: env, secrets, build, publish. |
| `scripts/sync-cloudflare-env.mjs` | `npm run cf:secrets`, `deploy-live.sh` | Parses an env file and runs `wrangler secret bulk` for Worker `stackwise-technologies`. Optional arg: path to env file (default `.env.prod`). |
| `scripts/migrate.mjs` | `npm run db:migrate`, `npm run db:migrate:prod` | Loads env files, connects to Neon, runs all `migrations/*.sql` in order. Set `MIGRATE_ENV=prod` to force `.env.prod`. |
| `scripts/db-reset-prod-payments.mjs` | `npm run db:reset:prod-payments` | Removes test/sandbox payment links from production Neon and marks remaining drafts as live (`kpay_is_test = false`). Dry-run by default; `--confirm` to apply. |

### Admin & payments (local)

- Admin dashboard: [http://localhost:3000/admin](http://localhost:3000/admin)
- Payment admin uses Neon Postgres, KPay, and SMTP — configure in `.env.local`.
- For local payment E2E, set `NEXT_PUBLIC_SITE_URL=http://localhost:3000` in `.env.local`.
- See [docs/PAYMENTS.md](docs/PAYMENTS.md) for payment flow, receipts, and legal notices.

### Payments legal & compliance

Customer payment pages (`/pay/*`) include a legal notice linking to [Payment processing & data](/pay/legal). See [docs/PAYMENTS.md](docs/PAYMENTS.md) for how payments, receipts, and personal data are handled.

---

Expand Down
61 changes: 61 additions & 0 deletions docs/PAYMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Payments — legal, receipts, and customer flow

## Customer payment pages

Public routes under `/pay/[slug]`:

| Route | Purpose |
| --- | --- |
| `/pay/[slug]` | Invoice + pay (Card / Mobile Money via KPay) |
| `/pay/[slug]/return` | KPay return handler |
| `/pay/[slug]/receipt` | **Paid only** — printable receipt (A4) |
| `/pay/legal` | Payment processing & data notice |

Every pay page includes a **PaymentLegalNotice** footer linking to `/pay/legal`.

## Payment processor

- **KPay** handles checkout (card + Mobile Money).
- Stackwise stores invoice metadata and payment status in Neon Postgres.
- Card numbers and mobile-money PINs are never stored by Stackwise.

## Audit logging

Customer interactions on pay pages log IP address and user agent to `payment_audit_logs`:

| Event | Trigger |
| --- | --- |
| `PAY_PAGE_VIEW` | Invoice page load |
| `PAYMENT_INIT` | Pay button / KPay redirect |
| `RETURN_CALLBACK` | KPay return URL |
| `RECEIPT_VIEW` | Receipt page (paid only) |

Logs are visible at `/admin/audit-logs` (latest 100 events) and on each payment-link detail page. Apply migration `0003_payment_audit_logs.sql` (or `npm run db:migrate`).

## Receipt access

Receipts are gated by payment status:

- `canAccessReceipt(link)` returns true only when `status === "PAID"`.
- Unpaid links redirect from `/pay/[slug]/receipt` to the pay page.
- Admin “View receipt” is shown only for paid links.

## Receipt email

When payment completes (webhook or return URL sync), `finalizePaidPayment`:

1. Marks the link `PAID`
2. Sends receipt email to the customer via `sendPaymentReceiptEmails`
3. Notifies admins listed in `ADMIN_NOTIFY_EMAILS`

The email includes a **receipt URL** (`/pay/[slug]/receipt`) for viewing and printing.

## Admin dashboard

- Live links: `kpay_is_test = false` only (production DB filter)
- Tables support **search** (invoice, customer, email) and **status filter**
- **10 rows per page** pagination

## Environment variables

See `.env.example` for `KPAY_*`, `DATABASE_URL`, `SMTP_*`, and `ADMIN_EMAILS`.
49 changes: 49 additions & 0 deletions migrations/0001_neon_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
CREATE TABLE IF NOT EXISTS admin_otps (
id UUID PRIMARY KEY,
subject_hash VARCHAR(64) NOT NULL,
otp VARCHAR(6) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_admin_otps_subject ON admin_otps(subject_hash);

CREATE TABLE IF NOT EXISTS otp_verify_attempts (
subject_hash VARCHAR(64) PRIMARY KEY,
failed_count INT NOT NULL DEFAULT 0,
locked_until TIMESTAMPTZ NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS otp_send_throttle (
subject_hash VARCHAR(64) PRIMARY KEY,
last_attempt_at TIMESTAMPTZ NULL,
last_sent_at TIMESTAMPTZ NULL
);

CREATE TABLE IF NOT EXISTS payment_links (
id UUID PRIMARY KEY,
slug VARCHAR(32) UNIQUE NOT NULL,
customer_name TEXT NOT NULL,
customer_email TEXT NOT NULL,
currency VARCHAR(8) NOT NULL DEFAULT 'XAF',
amount_usd NUMERIC(12, 2) NOT NULL,
amount_local NUMERIC(14, 2) NOT NULL,
exchange_rate NUMERIC(16, 8) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
line_items JSONB NOT NULL,
notes TEXT,
kpay_payment_id TEXT,
kpay_reference TEXT,
gateway_url TEXT,
invoice_number VARCHAR(32) UNIQUE NOT NULL,
sent_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
receipt_sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_payment_links_slug ON payment_links(slug);
CREATE INDEX IF NOT EXISTS idx_payment_links_status ON payment_links(status);
CREATE INDEX IF NOT EXISTS idx_payment_links_created ON payment_links(created_at DESC);
1 change: 1 addition & 0 deletions migrations/0002_kpay_is_test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS kpay_is_test BOOLEAN;
11 changes: 11 additions & 0 deletions migrations/0003_payment_audit_logs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS payment_audit_logs (
id UUID PRIMARY KEY,
payment_link_id UUID NOT NULL REFERENCES payment_links(id) ON DELETE CASCADE,
event VARCHAR(32) NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_payment_audit_link ON payment_audit_logs(payment_link_id, created_at DESC);
1 change: 1 addition & 0 deletions migrations/0004_allowed_payment_methods.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS allowed_payment_methods VARCHAR(20) NOT NULL DEFAULT 'BOTH';
7 changes: 7 additions & 0 deletions migrations/0005_manual_payment_fields.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS payment_source VARCHAR(20);
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS payment_method VARCHAR(32);
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS amount_received_usd NUMERIC(12, 2);
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS amount_received_local NUMERIC(14, 2);
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS collected_at TIMESTAMPTZ;
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS payment_reference TEXT;
ALTER TABLE payment_links ADD COLUMN IF NOT EXISTS payment_notes TEXT;
35 changes: 33 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.2.0",
"private": true,
"scripts": {
"db:migrate": "node scripts/migrate.mjs",
"dev": "next dev",
"build": "next build",
"start": "next start",
Expand All @@ -13,15 +14,21 @@
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"deploy:live": "bash scripts/deploy-live.sh",
"cf:secrets": "node scripts/sync-cloudflare-env.mjs .env.prod",
"db:migrate:prod": "MIGRATE_ENV=prod node scripts/migrate.mjs",
"db:reset:prod-payments": "MIGRATE_ENV=prod node scripts/db-reset-prod-payments.mjs",
"cf:whoami": "wrangler whoami",
"cf:status": "wrangler deployments status --name stackwise-technologies"
},
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
"next": "16.2.6",
"nodemailer": "^7.0.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@types/nodemailer": "^7.0.1",
"@opennextjs/cloudflare": "^1.20.2",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
Expand Down
Loading
Loading