From d6d6a366e3718058df7b8c2a63790d6bdd96c30b Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 26 Jun 2026 06:01:06 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20react-doctor=20label-has-associated-cont?= =?UTF-8?q?rol=209=E2=86=920,=20async-await-in-loop=203=E2=86=920=20(Promi?= =?UTF-8?q?se.allSettled),=20role-supports-aria-props=203=E2=86=920=20(alr?= =?UTF-8?q?eady=20done=20in=20prior=20batch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/communications/contacts.ts | 54 ++++-- src/actions/import-products.ts | 47 +++-- src/actions/square-orders.ts | 167 ++++++++++-------- src/components/admin/ProductEditForm.tsx | 52 +++--- src/components/admin/ScheduleImportModal.tsx | 4 +- src/components/admin/StopEditForm.tsx | 27 +-- .../admin/WaterLogEntryEditForm.tsx | 8 +- 7 files changed, 220 insertions(+), 139 deletions(-) diff --git a/src/actions/communications/contacts.ts b/src/actions/communications/contacts.ts index e28deae..9d828ef 100644 --- a/src/actions/communications/contacts.ts +++ b/src/actions/communications/contacts.ts @@ -340,15 +340,13 @@ await getSession(); const adminUser = await getAdminUser(); return { success: false, error: "Not authorized for this brand" }; } - const result: ImportResult = { created: 0, updated: 0, skipped: 0, errors: [] }; + const validRows = params.contacts.filter( + (row) => row.email || row.phone, + ); - for (const row of params.contacts) { - if (!row.email && !row.phone) { - result.skipped++; - continue; - } - try { - const r = await upsertContact({ + const settled = await Promise.allSettled( + validRows.map((row) => + upsertContact({ brand_id: params.brandId, email: row.email, phone: row.phone, @@ -361,16 +359,42 @@ await getSession(); const adminUser = await getAdminUser(); external_id: row.external_id, tags: row.tags, metadata: row._metadata, - }); - if (r.success) result.created++; - else { - result.errors.push({ row, error: r.error }); - } - } catch (err) { + }).then( + (r) => ({ row, r }), + (err) => ({ row, err }), + ), + ), + ); + + const result: ImportResult = { + created: 0, + updated: 0, + skipped: params.contacts.length - validRows.length, + errors: [], + }; + + for (const entry of settled) { + if (entry.status === "rejected") { + const err = (entry as PromiseRejectedResult).reason; result.errors.push({ - row, + row: validRows[settled.indexOf(entry)], error: err instanceof Error ? err.message : String(err), }); + continue; + } + const value = entry.value as + | { row: ContactImportEntry; r: { success: boolean; error?: string } } + | { row: ContactImportEntry; err: unknown }; + if ("err" in value) { + const err = value.err; + result.errors.push({ + row: value.row, + error: err instanceof Error ? err.message : String(err), + }); + } else if (value.r.success) { + result.created++; + } else { + result.errors.push({ row: value.row, error: value.r.error ?? "Unknown error" }); } } diff --git a/src/actions/import-products.ts b/src/actions/import-products.ts index 49d22da..3c5dd03 100644 --- a/src/actions/import-products.ts +++ b/src/actions/import-products.ts @@ -40,29 +40,56 @@ await getSession(); const adminUser = await getAdminUser(); let created = 0; const errors: { product: string; error: string }[] = []; + const validProducts: Array<{ name: string; description: string; price: number; type: string; active: boolean; image_url?: string }> = []; + const skipped: { product: { name: string; description: string; price: number; type: string; active: boolean; image_url?: string }; error: string }[] = []; for (const p of productsToImport) { const priceCents = Math.round(Number(p.price) * 100); if (!Number.isFinite(priceCents) || priceCents < 0) { - errors.push({ product: p.name, error: "Invalid price" }); - continue; + skipped.push({ product: p, error: "Invalid price" }); + } else { + validProducts.push(p); } - try { - await withBrand(brandId, (db) => + } + + const settled = await Promise.allSettled( + validProducts.map((p) => + withBrand(brandId, (db) => db.insert(products).values({ brandId: brandId, name: p.name, description: p.description ?? null, - priceCents, + priceCents: Math.round(Number(p.price) * 100), active: p.active, - }) - ); - created++; - } catch (err) { + }), + ).then( + () => p, + (err) => ({ p, err }), + ), + ), + ); + + for (const entry of settled) { + if (entry.status === "rejected") { + const err = (entry as PromiseRejectedResult).reason; errors.push({ - product: p.name, + product: "", error: err instanceof Error ? err.message : String(err), }); + continue; } + const value = entry.value; + if ("err" in value) { + errors.push({ + product: value.p.name, + error: value.err instanceof Error ? value.err.message : String(value.err), + }); + } else { + created++; + } + } + + for (const s of skipped) { + errors.push({ product: s.product.name, error: s.error }); } return { success: true, created, updated: 0, errors }; diff --git a/src/actions/square-orders.ts b/src/actions/square-orders.ts index 84b4859..d57bfff 100644 --- a/src/actions/square-orders.ts +++ b/src/actions/square-orders.ts @@ -101,82 +101,103 @@ await getSession(); const adminUser = await getAdminUser(); since ); - for (const payment of paymentsData.payments ?? []) { + interface SquarePayment { + id: string; + order_id?: string; + status?: string; + amount_money?: { amount?: string; currency?: string }; + customer_id?: string; + receipt_number?: string; + } + + const processablePayments = ((paymentsData.payments ?? []) as SquarePayment[]).filter( + (p) => Boolean(p.order_id) && completedStatuses.has(p.status ?? ""), + ); + + const orderResults = await Promise.allSettled( + processablePayments.map((p) => + fetchSquareOrder(settings.square_access_token!, p.order_id!).then( + (data) => ({ payment: p, orderData: data, err: undefined as unknown }), + (err: unknown) => ({ payment: p, orderData: undefined, err }), + ), + ), + ); + + for (const result of orderResults) { + if (result.status === "rejected") { + errors.push(`Square order fetch: ${String(result.reason)}`); + continue; + } + const { payment, orderData, err } = result.value; + if (err || !orderData) { + errors.push(`Payment ${payment.id}: ${String(err ?? "no order data")}`); + continue; + } + const order = orderData.order; + + // Build line items from Square order + const lineItems = (order.line_items ?? []).map((li: { + name: string; + quantity: string; + base_price_money: { amount: string; currency: string }; + }) => ({ + name: li.name, + quantity: parseInt(li.quantity, 10), + price: Number(li.base_price_money?.amount ?? 0) / 100, + })); + + // Compute total + const total = Number(order.total_money?.amount ?? payment.amount_money?.amount ?? 0) / 100; + + // Get customer info from payment + const customerName = payment.customer_id ?? "Square Customer"; + const customerEmail = payment.receipt_number ?? ""; + + // Use idempotency key to avoid duplicates + const idempotencyKey = `square_${payment.id}`; + + // Call SECURITY DEFINER RPC create_order_with_items + let createOk = false; + let errText = ""; try { - // Skip if no order_id - if (!payment.order_id) continue; - // Skip if already completed/canceled - if (!completedStatuses.has(payment.status ?? "")) continue; - - const orderData = await fetchSquareOrder(settings.square_access_token, payment.order_id); - const order = orderData.order; - - // Build line items from Square order - const lineItems = (order.line_items ?? []).map((li: { - name: string; - quantity: string; - base_price_money: { amount: string; currency: string }; - }) => ({ - name: li.name, - quantity: parseInt(li.quantity, 10), - price: Number(li.base_price_money?.amount ?? 0) / 100, - })); - - // Compute total - const total = Number(order.total_money?.amount ?? payment.amount_money?.amount ?? 0) / 100; - - // Get customer info from payment - const customerName = payment.customer_id ?? "Square Customer"; - const customerEmail = payment.receipt_number ?? ""; - - // Use idempotency key to avoid duplicates - const idempotencyKey = `square_${payment.id}`; - - // Call SECURITY DEFINER RPC create_order_with_items - let createOk = false; - let errText = ""; - try { - const rpcRes = await pool.query( - `SELECT * FROM create_order_with_items( - $1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10 - )`, - [ - idempotencyKey, - customerName, - customerEmail, - "", - null, // Square orders don't have RC stop_id - JSON.stringify( - lineItems.map((li: { name: string; quantity: number; price: number }) => ({ - id: null, // product lookup not available in this flow - quantity: li.quantity, - fulfillment: "shipping", - })) - ), - total, - "square", - "paid", - payment.id, - ] - ); - createOk = rpcRes.rows.length > 0; - } catch (e: unknown) { - const msg = e instanceof Error ? e.message : String(e); - // 409 / unique violation = already exists (idempotent) - if (/duplicate key|unique constraint|already exists/i.test(msg)) { - createOk = true; - } else { - errText = msg.slice(0, 100); - } - } - - if (createOk) { - synced++; + const rpcRes = await pool.query( + `SELECT * FROM create_order_with_items( + $1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9, $10 + )`, + [ + idempotencyKey, + customerName, + customerEmail, + "", + null, // Square orders don't have RC stop_id + JSON.stringify( + lineItems.map((li: { name: string; quantity: number; price: number }) => ({ + id: null, // product lookup not available in this flow + quantity: li.quantity, + fulfillment: "shipping", + })) + ), + total, + "square", + "paid", + payment.id, + ] + ); + createOk = rpcRes.rows.length > 0; + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + // 409 / unique violation = already exists (idempotent) + if (/duplicate key|unique constraint|already exists/i.test(msg)) { + createOk = true; } else { - errors.push(`Payment ${payment.id}: ${errText}`); + errText = msg.slice(0, 100); } - } catch (err) { - errors.push(`Payment ${payment.id}: ${String(err)}`); + } + + if (createOk) { + synced++; + } else { + errors.push(`Payment ${payment.id}: ${errText}`); } } } catch (err) { diff --git a/src/components/admin/ProductEditForm.tsx b/src/components/admin/ProductEditForm.tsx index 2583de0..5abada1 100644 --- a/src/components/admin/ProductEditForm.tsx +++ b/src/components/admin/ProductEditForm.tsx @@ -273,32 +273,36 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
- - +
- - +

Tax is calculated at checkout for shipping orders in your brand's nexus states. Non-taxable items (e.g. cooler boxes, apparel) are always exempt.

@@ -317,7 +321,7 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
- + Product Image

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

-
- - +
- + Headgate

{entry.headgate_name}

- + User

{entry.user_name}

@@ -106,11 +106,11 @@ export default function WaterLogEntryEditForm({ entry, backHref = "/admin/water- />
- + Submitted Via

{entry.submitted_via}

- + Logged

{new Date(entry.logged_at).toLocaleDateString("en-US", { month: "short",