fix: route gallery API through nginx, send Square receipts, unblock order completion
- nginx: add /photos and /uploads proxy routes to gallery-backend so the browser can reach the gallery API without needing direct port access - gallery.js: drop hardcoded port/subdomain fallbacks; use same-origin path via the new nginx routes - square.ts: pass buyerEmailAddress to createPayment so Square auto-sends a payment receipt to the customer on capture - square.ts: create fulfillments in RESERVED state (was PROPOSED) so staff can mark orders complete/filled directly from the Square dashboard - CartDrawer: merge Custom Vinyl into the Shape Balloon line item (one fewer Square line item per vinyl order); show modifier price deltas in cart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb878c2a8a
commit
4a135a7919
@ -299,6 +299,7 @@ export async function POST(req: NextRequest) {
|
|||||||
note,
|
note,
|
||||||
idempotencyKey: paymentIdempotencyKey,
|
idempotencyKey: paymentIdempotencyKey,
|
||||||
autocomplete: false, // hold only — capture after calendar write
|
autocomplete: false, // hold only — capture after calendar write
|
||||||
|
buyerEmailAddress: customerEmail,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!payment?.id) throw new Error('Pre-authorization returned no payment ID')
|
if (!payment?.id) throw new Error('Pre-authorization returned no payment ID')
|
||||||
|
|||||||
@ -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,
|
quantity: e.quantity,
|
||||||
priceCents: e.vinylShapePriceCents ?? 450,
|
priceCents: (e.vinylShapePriceCents ?? 450) + vinylCents,
|
||||||
catalogItemId: e.vinylShapeVariationId,
|
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: [
|
note: [
|
||||||
`Add-on for: ${e.product.name}`,
|
`Vinyl add-on for: ${e.product.name}`,
|
||||||
`Text: "${e.vinylText}"`,
|
`Text: "${e.vinylText}"`,
|
||||||
e.vinylFontName ? `Font: ${e.vinylFontName}` : null,
|
e.vinylFontName ? `Font: ${e.vinylFontName}` : null,
|
||||||
].filter(Boolean).join(' | ') || undefined,
|
].filter(Boolean).join(' | ') || undefined,
|
||||||
@ -423,10 +416,14 @@ export default function CartDrawer() {
|
|||||||
if (!optIds.length) return null
|
if (!optIds.length) return null
|
||||||
const ml = entry.product.modifiers?.find((m) => m.id === listId)
|
const ml = entry.product.modifiers?.find((m) => m.id === listId)
|
||||||
if (!ml) return null
|
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 (
|
return (
|
||||||
<div key={listId} style={{ fontSize: '0.8rem', color: '#555', marginTop: '0.2rem' }}>
|
<div key={listId} style={{ fontSize: '0.8rem', color: '#555', marginTop: '0.2rem' }}>
|
||||||
{ml.name}: {names.join(', ')}
|
{ml.name}: {labels.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -273,7 +273,7 @@ export async function createSquareOrder(params: {
|
|||||||
? params.fulfillment.type === 'delivery'
|
? params.fulfillment.type === 'delivery'
|
||||||
? [{
|
? [{
|
||||||
type: 'DELIVERY',
|
type: 'DELIVERY',
|
||||||
state: 'PROPOSED',
|
state: 'RESERVED',
|
||||||
deliveryDetails: {
|
deliveryDetails: {
|
||||||
recipient: {
|
recipient: {
|
||||||
displayName: params.fulfillment.recipientName,
|
displayName: params.fulfillment.recipientName,
|
||||||
@ -288,7 +288,7 @@ export async function createSquareOrder(params: {
|
|||||||
}]
|
}]
|
||||||
: [{
|
: [{
|
||||||
type: 'PICKUP',
|
type: 'PICKUP',
|
||||||
state: 'PROPOSED',
|
state: 'RESERVED',
|
||||||
pickupDetails: {
|
pickupDetails: {
|
||||||
recipient: {
|
recipient: {
|
||||||
displayName: params.fulfillment.recipientName,
|
displayName: params.fulfillment.recipientName,
|
||||||
@ -345,6 +345,7 @@ export async function createSquarePayment(params: {
|
|||||||
note: string
|
note: string
|
||||||
idempotencyKey: string
|
idempotencyKey: string
|
||||||
autocomplete?: boolean // false = pre-authorize (hold) without capturing
|
autocomplete?: boolean // false = pre-authorize (hold) without capturing
|
||||||
|
buyerEmailAddress?: string // triggers Square's automatic payment receipt email
|
||||||
}) {
|
}) {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const { result } = await client.paymentsApi.createPayment({
|
const { result } = await client.paymentsApi.createPayment({
|
||||||
@ -355,6 +356,7 @@ export async function createSquarePayment(params: {
|
|||||||
locationId: process.env.SQUARE_LOCATION_ID!,
|
locationId: process.env.SQUARE_LOCATION_ID!,
|
||||||
note: params.note,
|
note: params.note,
|
||||||
autocomplete: params.autocomplete ?? true,
|
autocomplete: params.autocomplete ?? true,
|
||||||
|
buyerEmailAddress: params.buyerEmailAddress,
|
||||||
})
|
})
|
||||||
return result.payment
|
return result.payment
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,16 +78,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const apiBaseCandidates = (() => {
|
const apiBaseCandidates = (() => {
|
||||||
const protocol = window.location.protocol;
|
|
||||||
const host = window.location.hostname;
|
|
||||||
const hints = [
|
const hints = [
|
||||||
window.GALLERY_API_URL || '',
|
window.GALLERY_API_URL || '',
|
||||||
'https://photobackend.beachpartyballoons.com',
|
'', // same-origin via nginx proxy
|
||||||
`${protocol}//${host}:5000`,
|
|
||||||
`${protocol}//${host}:5001`,
|
|
||||||
];
|
];
|
||||||
// Remove duplicates/empties
|
// Remove duplicates/empties — empty string means same-origin (/photos, /uploads)
|
||||||
return [...new Set(hints.filter(Boolean))];
|
return [...new Set(hints)];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
let activeApiBase = '';
|
let activeApiBase = '';
|
||||||
@ -219,9 +215,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const resolveUrl = (p) => {
|
const resolveUrl = (p) => {
|
||||||
if (typeof p !== 'string') return '';
|
if (typeof p !== 'string') return '';
|
||||||
if (p.startsWith('http') || p.startsWith('assets') || p.startsWith('/assets') || p.startsWith('../assets')) return p;
|
if (p.startsWith('http') || p.startsWith('assets') || p.startsWith('/assets') || p.startsWith('../assets')) return p;
|
||||||
const base = activeApiBase
|
const base = activeApiBase || '';
|
||||||
|| 'https://photobackend.beachpartyballoons.com'
|
|
||||||
|| `${window.location.protocol}//${window.location.hostname}:5000`;
|
|
||||||
const path = p.startsWith('/') ? p.slice(1) : p;
|
const path = p.startsWith('/') ? p.slice(1) : p;
|
||||||
return `${base.replace(/\/$/, '')}/${path}`;
|
return `${base.replace(/\/$/, '')}/${path}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,10 @@ http {
|
|||||||
server main-site:3050;
|
server main-site:3050;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstream gallery {
|
||||||
|
server gallery-backend:5000;
|
||||||
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
@ -38,6 +42,25 @@ http {
|
|||||||
location = /refund { return 301 /shop/refund; }
|
location = /refund { return 301 /shop/refund; }
|
||||||
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 ────────────────────────────────
|
# ── eStore: /shop and everything under it ────────────────────────────────
|
||||||
location ^~ /shop {
|
location ^~ /shop {
|
||||||
proxy_pass http://estore;
|
proxy_pass http://estore;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user