diff --git a/estore/src/app/api/checkout/route.ts b/estore/src/app/api/checkout/route.ts
index 1664ec5..2bbea90 100644
--- a/estore/src/app/api/checkout/route.ts
+++ b/estore/src/app/api/checkout/route.ts
@@ -293,12 +293,13 @@ export async function POST(req: NextRequest) {
// We capture only AFTER the calendar write succeeds. If the calendar fails,
// we void the hold — the customer is never charged without a confirmed booking.
const payment = await createSquarePayment({
- sourceId: sourceId,
- orderId: order.id!,
- amountMoney: { amount: order.totalMoney.amount!, currency: 'USD' },
+ sourceId: sourceId,
+ orderId: order.id!,
+ amountMoney: { amount: order.totalMoney.amount!, currency: 'USD' },
note,
- idempotencyKey: paymentIdempotencyKey,
- autocomplete: false, // hold only — capture after calendar write
+ idempotencyKey: paymentIdempotencyKey,
+ autocomplete: false, // hold only — capture after calendar write
+ buyerEmailAddress: customerEmail,
})
if (!payment?.id) throw new Error('Pre-authorization returned no payment ID')
diff --git a/estore/src/components/CartDrawer.tsx b/estore/src/components/CartDrawer.tsx
index 9b07238..7ec395f 100644
--- a/estore/src/components/CartDrawer.tsx
+++ b/estore/src/components/CartDrawer.tsx
@@ -272,19 +272,12 @@ export default function CartDrawer() {
}),
},
{
- name: `18" ${e.vinylShapeName ?? 'Shape'} Balloon`,
+ name: `18" ${e.vinylShapeName ?? 'Shape'} Balloon w/ Vinyl`,
quantity: e.quantity,
- priceCents: e.vinylShapePriceCents ?? 450,
+ priceCents: (e.vinylShapePriceCents ?? 450) + vinylCents,
catalogItemId: e.vinylShapeVariationId,
- note: `Vinyl add-on for: ${e.product.name}`,
- },
- {
- name: 'Custom Vinyl',
- quantity: e.quantity,
- priceCents: vinylCents,
- catalogItemId: e.product.variations[0]?.id ?? e.product.id,
note: [
- `Add-on for: ${e.product.name}`,
+ `Vinyl add-on for: ${e.product.name}`,
`Text: "${e.vinylText}"`,
e.vinylFontName ? `Font: ${e.vinylFontName}` : null,
].filter(Boolean).join(' | ') || undefined,
@@ -423,10 +416,14 @@ export default function CartDrawer() {
if (!optIds.length) return null
const ml = entry.product.modifiers?.find((m) => m.id === listId)
if (!ml) return null
- const names = optIds.map((id) => ml.options.find((o) => o.id === id)?.name ?? id)
+ const labels = optIds.map((id) => {
+ const opt = ml.options.find((o) => o.id === id)
+ if (!opt) return id
+ return opt.priceDelta ? `${opt.name} (+${fmt(opt.priceDelta)})` : opt.name
+ })
return (
- {ml.name}: {names.join(', ')}
+ {ml.name}: {labels.join(', ')}
)
})}
diff --git a/estore/src/lib/square.ts b/estore/src/lib/square.ts
index 427b873..37ca90b 100644
--- a/estore/src/lib/square.ts
+++ b/estore/src/lib/square.ts
@@ -273,7 +273,7 @@ export async function createSquareOrder(params: {
? params.fulfillment.type === 'delivery'
? [{
type: 'DELIVERY',
- state: 'PROPOSED',
+ state: 'RESERVED',
deliveryDetails: {
recipient: {
displayName: params.fulfillment.recipientName,
@@ -288,7 +288,7 @@ export async function createSquareOrder(params: {
}]
: [{
type: 'PICKUP',
- state: 'PROPOSED',
+ state: 'RESERVED',
pickupDetails: {
recipient: {
displayName: params.fulfillment.recipientName,
@@ -339,22 +339,24 @@ export async function createSquareOrder(params: {
}
export async function createSquarePayment(params: {
- sourceId: string
- orderId: string
- amountMoney: { amount: bigint; currency: string }
- note: string
- idempotencyKey: string
- autocomplete?: boolean // false = pre-authorize (hold) without capturing
+ sourceId: string
+ orderId: string
+ amountMoney: { amount: bigint; currency: string }
+ note: string
+ idempotencyKey: string
+ autocomplete?: boolean // false = pre-authorize (hold) without capturing
+ buyerEmailAddress?: string // triggers Square's automatic payment receipt email
}) {
const client = getClient()
const { result } = await client.paymentsApi.createPayment({
- sourceId: params.sourceId,
- idempotencyKey: params.idempotencyKey,
- amountMoney: params.amountMoney,
- orderId: params.orderId,
- locationId: process.env.SQUARE_LOCATION_ID!,
- note: params.note,
- autocomplete: params.autocomplete ?? true,
+ sourceId: params.sourceId,
+ idempotencyKey: params.idempotencyKey,
+ amountMoney: params.amountMoney,
+ orderId: params.orderId,
+ locationId: process.env.SQUARE_LOCATION_ID!,
+ note: params.note,
+ autocomplete: params.autocomplete ?? true,
+ buyerEmailAddress: params.buyerEmailAddress,
})
return result.payment
}
diff --git a/main-site/gallery/gallery.js b/main-site/gallery/gallery.js
index 63c6987..6deb395 100644
--- a/main-site/gallery/gallery.js
+++ b/main-site/gallery/gallery.js
@@ -78,16 +78,12 @@ document.addEventListener('DOMContentLoaded', () => {
};
const apiBaseCandidates = (() => {
- const protocol = window.location.protocol;
- const host = window.location.hostname;
const hints = [
window.GALLERY_API_URL || '',
- 'https://photobackend.beachpartyballoons.com',
- `${protocol}//${host}:5000`,
- `${protocol}//${host}:5001`,
+ '', // same-origin via nginx proxy
];
- // Remove duplicates/empties
- return [...new Set(hints.filter(Boolean))];
+ // Remove duplicates/empties — empty string means same-origin (/photos, /uploads)
+ return [...new Set(hints)];
})();
let activeApiBase = '';
@@ -219,9 +215,7 @@ document.addEventListener('DOMContentLoaded', () => {
const resolveUrl = (p) => {
if (typeof p !== 'string') return '';
if (p.startsWith('http') || p.startsWith('assets') || p.startsWith('/assets') || p.startsWith('../assets')) return p;
- const base = activeApiBase
- || 'https://photobackend.beachpartyballoons.com'
- || `${window.location.protocol}//${window.location.hostname}:5000`;
+ const base = activeApiBase || '';
const path = p.startsWith('/') ? p.slice(1) : p;
return `${base.replace(/\/$/, '')}/${path}`;
};
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 48c8bd3..cd112b0 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -16,6 +16,10 @@ http {
server main-site:3050;
}
+ upstream gallery {
+ server gallery-backend:5000;
+ }
+
server {
listen 80;
server_name _;
@@ -38,6 +42,25 @@ http {
location = /refund { return 301 /shop/refund; }
location = /refund/ { return 301 /shop/refund; }
+ # ── Gallery API and uploaded images ──────────────────────────────────────
+ location ^~ /photos {
+ proxy_pass http://gallery;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location ^~ /uploads {
+ proxy_pass http://gallery;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
# ── eStore: /shop and everything under it ────────────────────────────────
location ^~ /shop {
proxy_pass http://estore;