fix(checkout): create Postgres customers row on order completion (PayPal login bug)
Portal login + forgot-password read the Postgres customers table (bcrypt), NOT ERPNext. ensureCompliancePortalUser (the common path for Stripe/PayPal/crypto via handlePaymentComplete) only provisioned the ERPNext customer/website-user and never created the customers row -- so customers (notably PayPal, who reach this path directly) had no account to log into or reset a password against. Now upserts the customers row (no password; ON CONFLICT keeps any existing hash) with name + company so they can register/reset and log in immediately. Also: narrowed the placeholder-email skip from 'any synthetic@ or pipeline.com' to exactly 'synthetic@pipeline.com' (the FMCSA-census placeholder) so real customers on those real consumer domains aren't wrongly skipped -- which is what bit Paul Wilson. Added cc support to sendEmail. e2e-paypal-portal-fix.mjs is the regression test (seeds a compliance order, runs handlePaymentComplete, asserts the customers row is created). Rescue scripts for the affected customer included.
This commit is contained in:
parent
b437f66bc8
commit
9987b1e30d
5 changed files with 274 additions and 8 deletions
|
|
@ -36,11 +36,12 @@ function getTransporter(): nodemailer.Transporter {
|
|||
|
||||
// ─── Generic send ─────────────────────────────────────────────────────────────
|
||||
|
||||
export async function sendEmail(opts: { to: string; subject: string; html: string; text?: string }): Promise<void> {
|
||||
export async function sendEmail(opts: { to: string; subject: string; html: string; text?: string; cc?: string }): Promise<void> {
|
||||
const t = getTransporter();
|
||||
await t.sendMail({
|
||||
from: SMTP_FROM,
|
||||
to: opts.to,
|
||||
...(opts.cc ? { cc: opts.cc } : {}),
|
||||
subject: opts.subject,
|
||||
html: opts.html,
|
||||
text: opts.text || "",
|
||||
|
|
|
|||
|
|
@ -188,18 +188,40 @@ async function ensureCompliancePortalUser(
|
|||
if (!first) return;
|
||||
const email = ((first.customer_email as string) || "").toLowerCase().trim();
|
||||
const name = (first.customer_name as string) || email.split("@")[0] || "Customer";
|
||||
const company = (first.customer_company as string) || null;
|
||||
if (!email) return;
|
||||
|
||||
// Skip obviously non-deliverable placeholder addresses so we never create a
|
||||
// dead portal account or fire a set-password invite into the void. (e.g. the
|
||||
// FMCSA-census "synthetic@pipeline.com" placeholder used when no real email
|
||||
// was found.) These orders need a real email before onboarding.
|
||||
const domain = email.split("@")[1] || "";
|
||||
if (domain === "pipeline.com" || email.startsWith("synthetic@")) {
|
||||
console.warn(`[checkout] Skipping portal provisioning for ${orderId}: placeholder email ${email}`);
|
||||
// Skip only the genuine FMCSA-census placeholder, never a real customer who
|
||||
// happens to use these (real) consumer domains. The census placeholder is
|
||||
// exactly "synthetic@pipeline.com"; treat that one string as non-deliverable
|
||||
// and anything else as a real address.
|
||||
if (email === "synthetic@pipeline.com") {
|
||||
console.warn(`[checkout] Skipping portal provisioning for ${orderId}: FMCSA-census placeholder email`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Portal login account (Postgres `customers` row) ──────────────────
|
||||
// The portal login + forgot-password read the Postgres `customers` table
|
||||
// (bcrypt password_hash), NOT ERPNext. We create the row here (no password)
|
||||
// so the customer can immediately register/reset to set a password and log
|
||||
// in to track their order. The Stripe path historically relied on the
|
||||
// customer self-registering; PayPal/crypto orders reach here directly and
|
||||
// otherwise had NO customers row, which is why PayPal customers could not log
|
||||
// in or reset their password. Idempotent (ON CONFLICT keeps any existing
|
||||
// password_hash). See docs / Paul Wilson incident 2026-06-09.
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO customers (email, name, company)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (email) DO UPDATE SET
|
||||
name = COALESCE(customers.name, EXCLUDED.name),
|
||||
company = COALESCE(customers.company, EXCLUDED.company)`,
|
||||
[email, name, company],
|
||||
);
|
||||
} catch (custErr) {
|
||||
console.warn("[checkout] Could not upsert portal customers row:", custErr);
|
||||
}
|
||||
|
||||
// Provision the ERPNext Website User (idempotent) and link to the Customer.
|
||||
const { portalUserCreated } = await findOrCreateCustomer(email, name);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue