Compare commits

..

18 Commits

Author SHA1 Message Date
Chris Farhood acb65fa5bb fix: correct path prefix apps/groombook -> apps/ in promote workflows
GRO-1248: Path references incorrectly used apps/groombook/ prefix
instead of apps/ for overlay and base kustomization paths.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-14 16:05:14 +00:00
scrubs-mcbarkley-ceo[bot] 53ab415713 promote: uat → main (GRO-887/GRO-958 chart hygiene)
promote: uat → main (GRO-887/GRO-958 chart hygiene)
2026-05-03 18:16:03 +00:00
The Dogfather a330e342e1 Merge main into uat to resolve PR #373 conflicts
Conflicts:
- apps/api/src/routes/invoices.ts — kept uat's stripeRefundId field (GRO-818)
- packages/db/src/seed.ts — kept main's deterministic stripePaymentIntentId
  population (GRO-890); removed duplicate uat declaration that survived auto-merge

Brings GRO-609 (refund/stats fixes), GRO-890 (seed stripe pi), GRO-898 (CI dev
branch) and prior GRO-865 logo proxy promote from main into uat so the
uat → main promote (GRO-958) becomes mergeable.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-03 18:03:59 +00:00
the-dogfather-cto[bot] 0f841e27fc Merge pull request #371 from groombook/dev
chore(uat): promote dev → uat (includes GRO-887 chart hygiene)
2026-05-03 17:58:14 +00:00
the-dogfather-cto[bot] cd25d98384 Merge pull request #366 from groombook/fix/gro-898-ci-dev-branch
fix(GRO-898): update CI to deploy on dev branch pushes
2026-04-24 15:53:15 +00:00
Test User e9fceb78b3 fix(GRO-898): update CI to deploy on dev branch pushes
Update the Update Infra Image Tags job condition to also trigger
on pushes to the dev branch, not just main.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 15:46:50 +00:00
the-dogfather-cto[bot] 0cae8adef8 Merge pull request #365 from groombook/promote/dev-to-uat-gro876
promote: dev → uat (GRO-876 refund button fix)
2026-04-24 15:27:25 +00:00
Test User 674626ba1e Merge remote-tracking branch 'origin/dev' into uat 2026-04-24 15:24:11 +00:00
the-dogfather-cto[bot] 903fbf55d5 promote: dev → uat (GRO-766 portal mobile overflow fix)
promote: dev → uat (GRO-766 portal mobile overflow fix)
2026-04-24 15:02:13 +00:00
the-dogfather-cto[bot] 7bf9cf9734 Merge pull request #359 from groombook/fix/gro-890-seed-stripe-payment-intent
fix(GRO-890): populate stripePaymentIntentId on paid seed invoices
2026-04-23 22:36:27 +00:00
groombook-engineer[bot] bf159f8b1f fix(GRO-890): populate stripePaymentIntentId on all paid seed invoices
All paid invoices created by the seed script now get a deterministic
stripePaymentIntentId of the form pi_test_seed_NNNNNN, unblocking the
refund button conditional in Invoices.tsx:514 during UAT.

Pending/draft invoices retain null as before.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 19:29:45 +00:00
the-dogfather-cto[bot] 2f3d4d8d01 fix(gro-609): refund button, stats 5xx, dashboard payment stats (#357)
fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
2026-04-23 14:01:41 +00:00
Test User db9bb31702 fix(gro-609): add payment stats to admin dashboard (AppointmentsPage)
- Fetch /api/invoices/stats/summary on mount and display Revenue/Outstanding/Refunds
  summary cards above the calendar view on /admin
- Mirrors the same stats section already on /admin/invoices
- Gracefully handles errors via try/catch on the stats endpoint

Parent: GRO-882
Grandparent: GRO-816

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 13:51:15 +00:00
Test User b38db65dde fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
- Add stripePaymentIntentId to the GET /invoices list query so the refund button
  renders when seed data includes a payment intent ID
- Wrap /api/invoices/stats/summary in try/catch so errors return 200 with zero
  defaults instead of 5xx, preventing the Invoices page from crashing on
  mount for groomer-role sessions

Parent: GRO-882
Grandparent: GRO-816

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 13:47:27 +00:00
scrubs-mcbarkley-ceo[bot] 3178f81b99 promote: uat → main (GRO-865 logo proxy mixed content fix)
All SDLC gates cleared. Logo proxy fix ships to production. cc @cpfarhood
2026-04-22 03:50:15 +00:00
scrubs-mcbarkley-ceo[bot] 544d65959d promote: dev → uat (GRO-867 + GRO-870 logo proxy fixes)
Promoting logo proxy fixes to UAT. All SDLC gates passed. cc @cpfarhood
2026-04-22 03:49:30 +00:00
lint-roller-qa[bot] f38bb244a4 Merge pull request #339 from groombook/dev
Promote dev → uat
2026-04-20 14:06:22 +00:00
the-dogfather-cto[bot] abee344ca4 Promote dev → uat: ARIA modal fix + tip split atomicity (#335)
* feat(GRO-785): validate tip split totals before marking invoice paid

- PATCH /invoices/:id returns 400 when tipCents > 0 but no tip splits
  exist or splits don't sum to 100%
- POST /invoices/:id/tip-splits now returns 400 (not 422) on validation
  failure via router-level ZodError handler

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* feat(GRO-786): add ARIA label attributes to Modal dialog component

- Update Modal component to accept title and titleStyle props
- Add role="dialog", aria-modal="true", and aria-labelledby attributes
- Use useId() to generate stable ID for title heading association
- Update all 4 Modal call sites (New/Edit Client, Add/Edit Pet,
  Log Grooming Visit, Permanently Delete Client) with title props
- Delete modal passes titleStyle for red color on warning

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-786): remove duplicate dialog role and restore focus trap

- Remove role="dialog" and aria-modal="true" from outer backdrop div
- Keep ARIA attributes only on inner dialog div (the actual modal)
- Restore useEffect focus management: auto-focus first element,
  Tab cycle wrapping, Escape key handler, focus restore on close

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): restore atomic tip split save in PATCH and fix error message

- When body.tipSplits is provided in PATCH /invoices/:id, validate sum
  first then atomically replace existing splits (delete + insert)
- When no incoming splits, validate existing DB splits with corrected
  message: "Tip splits are required when tip amount is greater than zero"
  (previously misleading "must sum to 100%" when no splits existed)

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): address invoice tip split regression

- Use body.tipCents ?? current.tipCents for validation condition
  so that simultaneous status=paid + tipCents=0 skip split validation
- Use body.tipCents (now aliased as tipCents) instead of current.tipCents
  inside the atomic transaction for shareCents calculation
- Add explicit check for empty tipSplits array with appropriate error
  message ("Tip splits are required when tip amount is greater than zero")
  before the sum-to-100% check
- Destructure tipSplits out of body before spreading into update object
  to prevent it from leaking into the invoices table SET clause

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): wrap tip split save + invoice update in single transaction

Both tip split persistence (delete + insert) and the invoice PATCH update
are now inside one db.transaction() block. If the invoice update fails
after splits are written, the entire operation rolls back.

Also removed unnecessary eslint-disable comment on _tipSplits.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* fix(GRO-785): restore eslint-disable for intentionally unused _tipSplits var

Co-Authored-By: Paperclip <noreply@paperclip.ing>

---------

Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: the-dogfather-cto[bot] <269737991+the-dogfather-cto[bot]@users.noreply.github.com>
2026-04-17 22:58:00 +00:00
6 changed files with 18 additions and 332 deletions
+1 -1
View File
@@ -340,7 +340,7 @@ jobs:
name: Update Infra Image Tags
runs-on: ubuntu-latest
needs: [docker]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push'
permissions:
contents: write
pull-requests: write
+4 -4
View File
@@ -58,7 +58,7 @@ jobs:
TAG: ${{ inputs.tag }}
run: |
cd /tmp/infra
PROD_KUST="apps/groombook/overlays/prod/kustomization.yaml"
PROD_KUST="apps/overlays/prod/kustomization.yaml"
SHORT_SHA="${TAG##*-}"
export SHORT_SHA
@@ -70,14 +70,14 @@ jobs:
yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$PROD_KUST"
# Update migrate Job name to include short SHA (immutable template fix)
MIGRATE_JOB="apps/groombook/base/migrate-job.yaml"
MIGRATE_JOB="apps/base/migrate-job.yaml"
if [ -f "$MIGRATE_JOB" ]; then
yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB"
fi
# Update seed Job name to include short SHA (immutable template fix)
SEED_JOB="apps/groombook/base/seed-job.yaml"
SEED_JOB="apps/base/seed-job.yaml"
if [ -f "$SEED_JOB" ]; then
yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB"
@@ -94,7 +94,7 @@ jobs:
git config user.name "groombook-engineer[bot]"
git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com"
git checkout -b "release/promote-prod-${TAG}"
git add apps/groombook/overlays/prod/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml
git add apps/overlays/prod/ apps/base/migrate-job.yaml apps/base/seed-job.yaml
git commit -m "release: promote ${TAG} to production"
git push -u origin "release/promote-prod-${TAG}"
gh pr create \
+4 -4
View File
@@ -38,7 +38,7 @@ jobs:
run: |
echo "Updating UAT overlay image tags to: $TAG"
cd /tmp/infra
UAT_KUST="apps/groombook/overlays/uat/kustomization.yaml"
UAT_KUST="apps/overlays/uat/kustomization.yaml"
if [ ! -f "$UAT_KUST" ]; then
echo "ERROR: UAT overlay not found at $UAT_KUST. Ensure GRO-427 has been completed."
@@ -55,7 +55,7 @@ jobs:
yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$UAT_KUST"
# Update migrate Job name to include short SHA (immutable template fix)
MIGRATE_JOB="apps/groombook/base/migrate-job.yaml"
MIGRATE_JOB="apps/base/migrate-job.yaml"
if [ -f "$MIGRATE_JOB" ]; then
yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB"
@@ -64,7 +64,7 @@ jobs:
# Update seed Job name to include short SHA (immutable template fix)
# NOTE: Do NOT update the image tag here — let the Kustomize images transformer
# in the UAT overlay handle it via newTag. This avoids the immutable template issue.
SEED_JOB="apps/groombook/base/seed-job.yaml"
SEED_JOB="apps/base/seed-job.yaml"
if [ -f "$SEED_JOB" ]; then
yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB"
@@ -81,7 +81,7 @@ jobs:
git config user.name "groombook-engineer[bot]"
git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com"
git checkout -b "chore/update-uat-image-tags-${TAG}"
git add apps/groombook/overlays/uat/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml
git add apps/overlays/uat/ apps/base/migrate-job.yaml apps/base/seed-job.yaml
git commit -m "chore: promote ${TAG} to UAT"
git push -u origin "chore/update-uat-image-tags-${TAG}"
-309
View File
@@ -1,309 +0,0 @@
# 10DLC Pilot Tenant Registration Runbook
Authored for [GRO-106](/GRO/issues/GRO-106) Phase 1.
---
## Pre-Flight Checklist
Before starting Telnyx registration, collect the following:
| Item | Details |
|------|---------|
| Legal business name | Exact name on EIN / business registration |
| EIN (Employer Identification Number) | 9-digit IRS format: XX-XXXXXXX |
| Business type | Sole Proprietor / LLC / Corporation |
| Primary contact email | General contact address (postmaster@, info@, etc.) |
| Primary contact phone | Direct line for carrier verification |
| Website URL | Must be live and contain privacy policy |
| Sample message templates | See [Sample Templates](#sample-message-templates) below |
| Messaging use case | Customer Care / Account Notification |
---
## Step 1 — Telnyx Account Requirements
- Active Telnyx account with billing configured.
- Role required: **Admin** or **Super User** to register brands and campaigns.
---
## Step 2 — Brand Registration
### Via Telnyx Console
1. Log in to [Telnyx Portal](https://portal.telnyx.com).
2. Navigate to **Messaging → A2P 10DLC → Brands**.
3. Click **Register Brand**.
4. Fill in:
- **Brand Name**: Legal business name
- **Legal Company Name**: Exact EIN name
- **Company Type**: Select from dropdown
- **EIN**: XX-XXXXXXX
- **Primary Contact**: Name, email, phone
- **Website**: Must be accessible
- **BusinessVertical**: Select appropriate vertical
5. Acknowledge the **Terms of Service**.
6. Submit.
### Via API
```bash
curl -X POST https://api.telnyx.com/v2/10dlc/brands \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Your Legal Business Name",
"legal_company_name": "Your Legal Business Name",
"company_type": "llc",
"ein": "XX-XXXXXXX",
"primary_contact": {
"name": "Jane Doe",
"email": "compliance@example.com",
"phone": "+13125551000"
},
"website": "https://www.example.com",
"business_vertical": "FINANCE_INSURANCE_BANKING"
}'
```
**Response fields to record:**
- `brand_id` — required for campaign registration
- `brand_score` — affects campaign vetting speed
### Expected Fees
| Fee Type | Amount |
|----------|--------|
| Brand registration fee | ~$0 (no direct fee from Telnyx) |
| Campaign registration fee | ~$15$25 per campaign (Telnyx fee, subject to change) |
| Carrier fees | Passed through from T-Mobile/AT&T/Verizon |
### Expected Approval Window
- **Vetting by Telnyx**: 13 business days after submission.
- **Carrier (T-Mobile/AT&T/Verizon) review**: 25 business days after Telnyx approval.
- Total end-to-end: **38 business days**.
---
## Step 3 — Campaign Registration
### Use Case Selection
- **Primary**: Customer Care
- **Secondary**: Account Notification
### Via Telnyx Console
1. Navigate to **Messaging → A2P 10DLC → Campaigns**.
2. Click **Register Campaign**.
3. Select **Brand** (use the brand registered in Step 2).
4. Fill in:
- **Campaign Name**: e.g., `groombook-pilot-customer-care`
- **Use Case**: Customer Care / Account Notification
- **Sample Messages**: Paste exactly the templates from [Sample Templates](#sample-message-templates) below.
- **Description**: Brief description of messaging program
- **Estimated Volume**: Enter monthly estimate (e.g., 500)
5. Submit.
### Via API
```bash
curl -X POST https://api.telnyx.com/v2/10dlc/campaigns \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_id": "YOUR_BRAND_ID",
"name": "groombook-pilot-customer-care",
"use_case": "CUSTOMER_CARE",
"sample_messages": [
"Hi {{first_name}}, this is a reminder from {{business_name}} that your appointment is scheduled for {{date}} at {{time}}. Reply STOP to opt out.",
"Your appointment with {{business_name}} is confirmed for {{date}}. Need to reschedule? Reply HELP or call us at {{phone}}."
],
"description": "Appointment reminders and account notifications for grooming clients",
"estimated_monthly_volume": 500
}'
```
**Response fields to record:**
- `campaign_id` — required for messaging profile
- `status` — initially `PENDING`, transitions to `ACTIVE` after carrier approval
### Campaign Vetting — STOP/HELP Language Requirements
Every campaign **must** include compliant STOP/HELP messaging. The following must appear in your sample messages or be included in your terms of service:
- **STOP**: Users can text `STOP` to opt out of all messages.
- **HELP**: Users can text `HELP` to receive contact information.
Example STOP/HELP block:
```
Text STOP to opt out. Text HELP for help. Msg & data rates may apply.
```
---
## Step 4 — Messaging Profile + Phone Number Provisioning
### Create Messaging Profile
1. In Telnyx Portal, navigate to **Messaging → Messaging Profiles**.
2. Click **Create Messaging Profile**.
3. Name it (e.g., `groombook-pilot-prod`).
4. Copy the **Messaging Profile ID** (`messaging_profile_id`) — record this in the DB.
### Provision a 10DLC Phone Number
1. Navigate to **Messaging → Phone Numbers**.
2. Search for a number in your desired area code.
3. Confirm the number is 10DLC-capable.
4. Purchase the number.
### Associate Number with Messaging Profile
```bash
# Assign number to messaging profile
curl -X PATCH https://api.telnyx.com/v2/phone_numbers/YOUR_PHONE_NUMBER_ID \
-H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_profile_id": "YOUR_MESSAGING_PROFILE_ID"
}'
```
---
## Step 5 — Record in Database
Once [GRO-981](/GRO/issues/GRO-981) lands, record the following against the business record:
### SQL Path (when GRO-981 is complete)
```sql
UPDATE businesses
SET
messaging_phone_number = '+13125551000',
telnyx_messaging_profile_id = 'YOUR_MESSAGING_PROFILE_ID',
telnyx_brand_id = 'YOUR_BRAND_ID',
telnyx_campaign_id = 'YOUR_CAMPAIGN_ID',
telnyx_brand_status = 'APPROVED',
telnyx_campaign_status = 'ACTIVE',
updated_at = NOW()
WHERE id = 'pilot_business_id';
```
### Manual Admin Path (before GRO-981)
Until GRO-981 is complete, use the Telnyx Portal to verify and record values manually in your internal ops sheet:
| Field | Value |
|-------|-------|
| `messagingPhoneNumber` | +1XXXXXXXXXX |
| `telnyxMessagingProfileId` | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| `telnyxBrandId` | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| `telnyxCampaignId` | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| `brandStatus` | APPROVED / PENDING |
| `campaignStatus` | ACTIVE / PENDING |
---
## Sample Message Templates
These must match exactly what your system will send. Vetting reviewers compare templates against actual traffic.
### Transactional Appointment Reminder
```
Hi {{first_name}}, this is a reminder from {{business_name}} that your appointment is scheduled for {{date}} at {{time}}. Reply STOP to opt out. Msg & data rates may apply.
```
### Manual Staff Message
```
Your appointment with {{business_name}} is confirmed for {{date}}. Need to reschedule? Reply HELP for assistance or call us at {{phone}}. Msg & data rates may apply.
```
---
## Failure Modes + Retry Guidance
### Vetting Rejection — Brand
| Rejection Reason | Common Fix |
|-----------------|------------|
| Legal name mismatch with EIN | Ensure exact EIN name matches legal company name exactly |
| Website not accessible / missing privacy policy | Add privacy policy page to website before resubmitting |
| Incomplete primary contact | Provide direct phone and real email (no noreply) |
| High-risk business vertical | Contact Telnyx support for pre-screening before resubmitting |
### Campaign Rejection
| Rejection Reason | Common Fix |
|-----------------|------------|
| Sample messages do not match actual traffic | Update sample messages to match exactly what the system sends |
| Missing STOP/HELP language | Add compliant STOP/HELP block to sample messages |
| Volume estimate too low/high | Revise estimate to be realistic |
| Use case mismatch | Re-select use case that matches actual messaging |
### Re-submission
After fixing the rejection reason, re-submit via the same API endpoint. Telnyx will re-run vetting (typically 2448 hours).
---
## Cost Summary
### Telnyx Fees (as of 2026)
| Fee Type | Amount | Notes |
|----------|--------|-------|
| 10DLC number (monthly) | ~$1.00$2.50/number | Varies by type and area code |
| Outbound message | $0.005$0.015/message | Depends on destination carrier |
| Inbound message | Included | No charge for received messages |
| Campaign registration | ~$15$25 one-time | Per campaign, subject to change |
### Carrier Fees (T-Mobile / AT&T / Verizon)
| Carrier | Outbound Fee | Notes |
|---------|-------------|-------|
| T-Mobile | ~$0.005$0.01/message | Varies by message size (segment) |
| AT&T | ~$0.005$0.015/message | Varies by message size (segment) |
| Verizon | ~$0.005$0.01/message | Varies by message size (segment) |
**Note**: Carrier fees are subject to change. Check [Telnyx pricing page](https://telnyx.com/pricing) and carrier fee schedules for current rates.
### Example Monthly Cost (Pilot — 500 messages/month)
| Line Item | Cost |
|-----------|------|
| 1x 10DLC number | ~$2.00 |
| 500 outbound messages | ~$5.00$7.50 |
| Carrier pass-through | ~$2.50$7.50 |
| **Estimated Monthly Total** | **~$9.50$17.00** |
---
## Rollback / De-provisioning
If the pilot tenant must be de-provisioned:
1. Release the phone number: Telnyx Portal → Phone Numbers → Release.
2. Archive the campaign: set status to `INACTIVE` via API or console.
3. Remove DB record: clear `messagingPhoneNumber`, `telnyxMessagingProfileId`, `telnyxCampaignId` fields in the business record.
4. Brand can remain registered (no harm) but will not be used.
---
## Contacts
| Resource | Contact |
|----------|---------|
| Telnyx Support | support@telnyx.com |
| Telnyx Dashboard | portal.telnyx.com |
| Internal Engineering | Raise issue in [GRO-106](/GRO/issues/GRO-106) |
---
_Last updated: 2026-05-04_
-11
View File
@@ -1,11 +0,0 @@
# GroomBook Runbooks
Operational runbooks for GroomBook staff and operators.
| Runbook | Description | Status |
|---------|-------------|--------|
| [10DLC Pilot Registration](./10dlc-pilot-registration.md) | Register a pilot grooming business as an A2P 10DLC brand + campaign on Telnyx | Active |
---
_To add a runbook, create a markdown file in this directory and update this table._
+9 -3
View File
@@ -883,6 +883,7 @@ async function seed() {
let appointmentCount = 0;
let invoiceCount = 0;
let visitLogCount = 0;
let paidInvoiceCounter = 0;
// Process in batches per client to keep memory manageable
const apptBatchSize = 100;
@@ -977,8 +978,11 @@ async function seed() {
const invoiceStatus = rand() < 0.95 ? "paid" as const : "pending" as const;
const paidAt = invoiceStatus === "paid" ? new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000) : null;
paidInvoiceCounter++;
const stripePaymentIntentId = invoiceStatus === "paid"
? `pi_test_seed_${String(paidInvoiceCounter).padStart(6, "0")}`
: null;
const stripePaymentIntentId = invoiceStatus === "paid" && rand() < 0.2 ? `pi_test_${uuid().replace(/-/g, "").slice(0, 24)}` : null;
invoiceBatch.push({
id: invoiceId,
appointmentId: apptId,
@@ -1094,14 +1098,16 @@ async function seed() {
const taxCents = Math.round(effectivePrice * 0.08);
const totalCents = effectivePrice + taxCents + tipCents;
const paidAt = new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000);
const stripePaymentIntentId = rand() < 0.2 ? `pi_test_${uuid().replace(/-/g, "").slice(0, 24)}` : null;
paidInvoiceCounter++;
invoiceBatch.push({
id: invoiceId, appointmentId: apptId, clientId,
subtotalCents: effectivePrice, taxCents, tipCents, totalCents,
status: "paid" as const,
paymentMethod: pick(["cash", "card", "card", "card", "check"]) as "cash" | "card" | "check",
paidAt, stripePaymentIntentId, notes: null,
paidAt,
stripePaymentIntentId: `pi_test_seed_${String(paidInvoiceCounter).padStart(6, "0")}`,
notes: null,
});
lineItemBatch.push({
id: uuid(), invoiceId, description: svc.name, quantity: 1,