top of page

import { processSuccessfulPaymentByPaymentId } from "backend/orders"; export async function wixPay_onPaymentUpdate(event) { if (event.status !== "Successful") { return; } const paymentId = event.paymentId || event._id || (event.payment && event.payment.id); if (!paymentId) { throw new Error(`Successful payment event did not include a payment ID: ${JSON.stringify(event)}`); } await processSuccessfulPaymentByPaymentId(paymentId); }

const REQUIRED_FIELDS = [ "firstName", "lastName", "email", "currentSituation", "primarySkills", "strengths", "interests", "lifeSituation", "targetCustomer", "workPreference", "onlinePlatforms", "networkAccess", "timeAvailable", "urgency", "incomeGoal", "offerComfort", "priorAttempts", "avoid", "serviceSelection" ]; const OPTIONAL_FIELDS = [ "resources", "extraContext" ]; export function validateIntakeData(input) { const data = {}; const errors = {}; REQUIRED_FIELDS.forEach((field) => { const value = Array.isArray(input[field]) ? input[field].map((item) => String(item).trim()).filter(Boolean).join(", ") : String(input[field] || "").trim(); data[field] = value; if (!value) errors[field] = "Required"; }); OPTIONAL_FIELDS.forEach((field) => { data[field] = String(input[field] || "").trim(); }); data.consent = Boolean(input.consent); if (!data.consent) errors.consent = "Required"; if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { errors.email = "Invalid email"; } return { ok: Object.keys(errors).length === 0, data, errors }; }

import wixData from "wix-data"; import { createPayment } from "wix-pay-backend"; import { getPlanPrice } from "backend/pricing"; import { validateIntakeData } from "backend/validation"; import { generateClientPlan } from "backend/planGenerator"; import { sendPlanEmail } from "backend/email"; const COLLECTIONS = { submissions: "ClientIntakeSubmissions", orders: "Orders", plans: "GeneratedPlans" }; export async function createSubmissionAndPayment(rawIntakeData) { const validation = validateIntakeData(rawIntakeData); if (!validation.ok) { throw new Error(`Intake validation failed: ${JSON.stringify(validation.errors)}`); } const price = getPlanPrice(); const submission = await wixData.insert(COLLECTIONS.submissions, { ...validation.data, source: "custom-velo-intake", status: "submitted" }, { suppressAuth: true }); const order = await wixData.insert(COLLECTIONS.orders, { submissionId: submission._id, customerEmail: validation.data.email, amountCents: price.amountCents, currency: price.currency, status: "pending" }, { suppressAuth: true }); const payment = await createPayment({ amount: price.amount, currency: price.currency, items: [{ name: price.name, price: price.amount, quantity: 1 }] }); await wixData.update(COLLECTIONS.orders, { ...order, wixPaymentId: payment.id }, { suppressAuth: true }); await wixData.update(COLLECTIONS.submissions, { ...submission, orderId: order._id }, { suppressAuth: true }); return { paymentId: payment.id, orderId: order._id, submissionId: submission._id }; } export async function processSuccessfulPaymentByPaymentId(paymentId) { const orderResult = await wixData.query(COLLECTIONS.orders) .eq("wixPaymentId", paymentId) .limit(1) .find({ suppressAuth: true }); if (!orderResult.items.length) { throw new Error(`No order found for payment ${paymentId}`); } const order = orderResult.items[0]; if (order.status === "paid") { return { skipped: true, reason: "Order already paid", orderId: order._id }; } const submission = await wixData.get(COLLECTIONS.submissions, order.submissionId, { suppressAuth: true }); const intakeData = { firstName: submission.firstName, lastName: submission.lastName, email: submission.email, currentSituation: submission.currentSituation, primarySkills: submission.primarySkills, strengths: submission.strengths, interests: submission.interests, lifeSituation: submission.lifeSituation, targetCustomer: submission.targetCustomer, workPreference: submission.workPreference, onlinePlatforms: submission.onlinePlatforms, networkAccess: submission.networkAccess, timeAvailable: submission.timeAvailable, urgency: submission.urgency, incomeGoal: submission.incomeGoal, offerComfort: submission.offerComfort, priorAttempts: submission.priorAttempts, resources: submission.resources, avoid: submission.avoid, extraContext: submission.extraContext, serviceSelection: submission.serviceSelection, consent: submission.consent }; const plan = await generateClientPlan(intakeData); const generated = await wixData.insert(COLLECTIONS.plans, { submissionId: submission._id, orderId: order._id, customerEmail: submission.email, planContent: plan, status: "generated" }, { suppressAuth: true }); let planStatus = "emailed"; const planUpdate = { ...generated, status: planStatus, emailedAt: new Date() }; try { await sendPlanEmail({ to: submission.email, firstName: submission.firstName, plan }); } catch (error) { planStatus = "email_failed"; planUpdate.status = planStatus; planUpdate.emailError = String(error && error.message ? error.message : error); delete planUpdate.emailedAt; } await wixData.update(COLLECTIONS.plans, planUpdate, { suppressAuth: true }); await wixData.update(COLLECTIONS.orders, { ...order, status: "paid", paidAt: new Date() }, { suppressAuth: true }); await wixData.update(COLLECTIONS.submissions, { ...submission, status: "plan_generated" }, { suppressAuth: true }); return { orderId: order._id, planId: generated._id }; } export async function getPlanByOrderId(orderId) { const planResult = await wixData.query(COLLECTIONS.plans) .eq("orderId", orderId) .limit(1) .find({ suppressAuth: true }); const order = await wixData.get(COLLECTIONS.orders, orderId, { suppressAuth: true }); const plan = planResult.items[0]; return { orderId, paymentStatus: order.status, planStatus: plan ? plan.status : "not generated", planContent: plan ? plan.planContent : "", customerEmail: order.customerEmail }; } export async function listSubmissionStatusRows() { const submissions = await wixData.query(COLLECTIONS.submissions) .descending("_createdDate") .limit(100) .find({ suppressAuth: true }); const orders = await wixData.query(COLLECTIONS.orders) .limit(100) .find({ suppressAuth: true }); const plans = await wixData.query(COLLECTIONS.plans) .limit(100) .find({ suppressAuth: true }); return submissions.items.map((submission) => { const order = orders.items.find((item) => item.submissionId === submission._id); const plan = plans.items.find((item) => item.submissionId === submission._id); return { _id: submission._id, createdAt: submission._createdDate, name: submission.firstName, email: submission.email, urgency: submission.urgency, incomeGoal: submission.incomeGoal, paymentStatus: order ? order.status : "missing", planStatus: plan ? plan.status : "not generated", orderId: order ? order._id : "" }; }); }

<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style> :root { color-scheme: light; --green: #1f7a4d; --green-dark: #155c3a; --gold: #d9a441; --ink: #1f2933; --muted: #667085; --line: #d8ded6; --paper: #fbfaf6; --white: #ffffff; --soft: #f4f7f1; } * { box-sizing: border-box; } body { margin: 0; background: var(--paper); color: var(--ink); font-family: Arial, Helvetica, sans-serif; } .shell { max-width: 760px; margin: 0 auto; padding: 26px 16px 30px; } .eyebrow { color: var(--green); font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } h1 { margin: 8px 0 8px; font-size: clamp(25px, 6vw, 38px); line-height: 1.05; letter-spacing: 0; } p { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.45; } form { margin-top: 18px; background: var(--white); border: 1px solid var(--line); border-radius: 8px; padding: 18px; box-shadow: 0 14px 36px rgba(31, 41, 51, .08); } .progress { display: grid; gap: 8px; margin-bottom: 16px; } .progress-meta { display: flex; justify-content: space-between; gap: 12px; color: var(--muted); font-size: 13px; font-weight: 700; } .bar { height: 8px; overflow: hidden; border-radius: 999px; background: var(--soft); } .bar span { display: block; height: 100%; width: 20%; border-radius: inherit; background: linear-gradient(90deg, var(--green), var(--gold)); transition: width .2s ease; } .step { display: none; } .step.active { display: grid; gap: 13px; } h2 { margin: 0; font-size: 20px; line-height: 1.2; } label { display: grid; gap: 6px; font-size: 13px; font-weight: 800; } input, select, textarea { width: 100%; border: 1px solid var(--line); border-radius: 6px; padding: 11px 12px; color: var(--ink); font: inherit; font-weight: 400; background: #fff; } textarea { min-height: 92px; resize: vertical; } .hint { color: var(--muted); font-size: 12px; font-weight: 400; line-height: 1.4; } .consent { display: flex; gap: 10px; align-items: flex-start; font-weight: 400; color: var(--muted); } .consent input { width: auto; margin-top: 3px; } .actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; } button { border: 0; border-radius: 6px; padding: 13px 16px; background: var(--green); color: white; font: inherit; font-weight: 800; cursor: pointer; } button:hover { background: var(--green-dark); } button.secondary { background: var(--soft); color: var(--green-dark); } button.secondary:hover { background: #e7eee3; } button:disabled { cursor: not-allowed; opacity: .55; } .status { min-height: 22px; margin-top: 12px; color: var(--green-dark); font-size: 13px; font-weight: 800; } .error { color: #b42318; } @media (max-width: 520px) { .shell { padding: 18px 10px 24px; } form { padding: 15px; } .actions { grid-template-columns: 1fr; } } </style> </head> <body> <main class="shell"> <section> <div class="eyebrow">ProfitFromSkills intake</div> <h1>Start Your First Paying Client Action Plan</h1> <p>Answer a few focused questions. Your plan will recommend a simple offer, a realistic first customer, outreach scripts, and a 7 to 14 day action path.</p> </section> <form id="intakeForm"> <div class="progress" aria-hidden="true"> <div class="progress-meta"> <span id="stepTitle">Step 1 of 5</span> <span id="stepName">Basics</span> </div> <div class="bar"><span id="progressBar"></span></div> </div> <section class="step active" data-name="Basics"> <h2>Your contact details</h2> <label>First name <input name="firstName" autocomplete="given-name" required /></label> <label>Last name <input name="lastName" autocomplete="family-name" required /></label> <label>Email <input name="email" type="email" autocomplete="email" required /></label> <label> Current life or work situation <select name="lifeSituation" required> <option value="">Choose one</option> <option>Employed</option> <option>Self-employed/freelancing</option> <option>Between jobs</option> <option>Student</option> <option>Side hustle</option> <option>Other</option> </select> </label> </section> <section class="step" data-name="Skills"> <h2>What you can already do</h2> <label>Describe your current situation <textarea name="currentSituation" required></textarea></label> <label>Primary skills <textarea name="primarySkills" required></textarea></label> <label>Top strengths <textarea name="strengths" required></textarea></label> <label>Interests or hobbies <textarea name="interests" required></textarea></label> </section> <section class="step" data-name="Market"> <h2>Your likely first customer</h2> <label>Who do you think you could help first? <textarea name="targetCustomer" required></textarea></label> <label>How do you prefer working with people? <input name="workPreference" placeholder="Done-for-you, coaching, templates, group help..." required /></label> <label>Where are you active online? <input name="onlinePlatforms" placeholder="LinkedIn, Facebook, Instagram, email, local groups..." required /></label> <label>Who can you realistically reach this week? <textarea name="networkAccess" required></textarea></label> </section> <section class="step" data-name="Goals"> <h2>Your time, goal, and comfort level</h2> <label> Time available per day <select name="timeAvailable" required> <option value="">Choose one</option> <option>Less than 1 hour</option> <option>1-2 hours</option> <option>2-4 hours</option> <option>4+ hours</option> </select> </label> <label> Urgency <select name="urgency" required> <option value="">Choose one</option> <option>7 days</option> <option>14 days</option> <option>30 days</option> </select> </label> <label> 30-day income goal <select name="incomeGoal" required> <option value="">Choose one</option> <option>$100-$500</option> <option>$500-$1,000</option> <option>$1,000-$2,500</option> <option>$2,500+</option> </select> </label> <label>What kind of selling feels most doable? <input name="offerComfort" placeholder="Warm messages, DMs, email, referrals, local outreach..." required /></label> </section> <section class="step" data-name="Final"> <h2>Final details</h2> <label>Have you tried making money with these skills before? <textarea name="priorAttempts" required></textarea></label> <label>Existing resources <textarea name="resources" placeholder="Audience, contacts, tools, portfolio links, credentials, examples..."></textarea></label> <label>Anything you want to avoid? <textarea name="avoid" required></textarea></label> <label>Anything else we should know? <textarea name="extraContext"></textarea></label> <label>Service <select name="serviceSelection" required><option>Starter Plan - $37</option></select></label> <label class="consent"> <input name="consent" type="checkbox" required /> <span>I agree to receive my custom plan by email and understand this is strategic guidance, not a guarantee of income.</span> </label> <p class="hint">After payment, your intake will be used to prepare the action plan and email it to you automatically.</p> </section> <div class="actions"> <button id="backButton" class="secondary" type="button" disabled>Back</button> <button id="nextButton" type="button">Next</button> </div> <div id="status" class="status" aria-live="polite"></div> </form> </main> <script> const form = document.getElementById("intakeForm"); const steps = Array.from(document.querySelectorAll(".step")); const backButton = document.getElementById("backButton"); const nextButton = document.getElementById("nextButton"); const stepTitle = document.getElementById("stepTitle"); const stepName = document.getElementById("stepName"); const progressBar = document.getElementById("progressBar"); const status = document.getElementById("status"); let stepIndex = 0; function showStep(index) { stepIndex = Math.max(0, Math.min(index, steps.length - 1)); steps.forEach((step, current) => step.classList.toggle("active", current === stepIndex)); backButton.disabled = stepIndex === 0; nextButton.textContent = stepIndex === steps.length - 1 ? "Continue to Payment" : "Next"; stepTitle.textContent = `Step ${stepIndex + 1} of ${steps.length}`; stepName.textContent = steps[stepIndex].dataset.name || ""; progressBar.style.width = `${((stepIndex + 1) / steps.length) * 100}%`; status.textContent = ""; status.className = "status"; } function validateCurrentStep() { const fields = Array.from(steps[stepIndex].querySelectorAll("input, select, textarea")); const invalid = fields.find((field) => !field.checkValidity()); if (invalid) { invalid.reportValidity(); invalid.focus(); return false; } return true; } function submitForm() { if (!form.checkValidity()) { form.reportValidity(); return; } status.className = "status"; status.textContent = "Preparing secure checkout..."; const formData = new FormData(form); const intakeData = Object.fromEntries(formData.entries()); intakeData.consent = formData.get("consent") === "on"; window.parent.postMessage({ type: "profitfromskills:intake-submit", intakeData }, "*"); } backButton.addEventListener("click", () => showStep(stepIndex - 1)); nextButton.addEventListener("click", () => { if (!validateCurrentStep()) return; if (stepIndex === steps.length - 1) { submitForm(); return; } showStep(stepIndex + 1); }); form.addEventListener("submit", (event) => { event.preventDefault(); submitForm(); }); window.addEventListener("message", (event) => { const message = event.data || {}; if (message.type !== "profitfromskills:status") return; status.textContent = message.message || ""; status.className = message.kind === "error" ? "status error" : "status"; }); showStep(0); </script> </body> </html>

bottom of page