🎥 PodcastsDesigned in California Makes Its Official Debut(03.09.2026 um 17:59 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🍏 iOS / Mac OSWill Siri AI Speak Hindi? What Apple Has Published for India(11.09.2026 um 05:15 Uhr)
🍏 iOS / Mac OSiPhone Duo Apps Could Make or Break Apple’s Foldable iPhone(11.09.2026 um 05:16 Uhr)
🎥 PodcastsDesigned in California Makes Its Official Debut(03.09.2026 um 17:59 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🍏 iOS / Mac OSWill Siri AI Speak Hindi? What Apple Has Published for India(11.09.2026 um 05:15 Uhr)
🍏 iOS / Mac OSiPhone Duo Apps Could Make or Break Apple’s Foldable iPhone(11.09.2026 um 05:16 Uhr)

🔧 Programmierung 🕛 vor 3 Monaten 10 Min Lesezeit
0

E-commerce Order Automation: Stripe + Invoice + Shipping Workflow

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Before I automated order processing at .






Step 2: The BullMQ Worker



The worker runs as a separate long-lived process. It pulls jobs off the queue and runs the pipeline steps in order.




CODE
// workers/order-processor.ts
import { Worker, Job } from "bullmq";
import { redis } from "@/lib/redis";
import { fetchOrderDetails } from "@/lib/stripe";
import { createZohoDeal } from "@/lib/zoho";
import { logAirtableOrder } from "@/lib/airtable";
import { createPostNordShipment } from "@/lib/postnord";
import { createNetvisorInvoice } from "@/lib/netvisor";
import { sendConfirmationEmail } from "@/lib/mailgun";
import { notifyTelegram } from "@/lib/telegram";

interface OrderJobData {
paymentIntentId: string;
eventId: string;
}

const worker = new Worker<OrderJobData>(
"orders",
async (job: Job<OrderJobData>) => {
const { paymentIntentId } = job.data;

// Fetch full order details from Stripe (customer, line items, shipping)
const order = await fetchOrderDetails(paymentIntentId);

// Each step returns data needed by subsequent steps.
// Failures throw — BullMQ handles retry with backoff.
const { dealId } = await createZohoDeal(order);
await logAirtableOrder(order, { dealId });
const { trackingNumber, labelUrl } = await createPostNordShipment(order);
const { invoiceNumber } = await createNetvisorInvoice(order, { trackingNumber });

await sendConfirmationEmail(order, { trackingNumber, labelUrl, invoiceNumber });

return { dealId, trackingNumber, invoiceNumber };
},
{ connection: redis, concurrency: 3 }
);

worker.on("failed", async (job, err) => {
if (!job) return;

// Alert on final failure (all retries exhausted)
if (job.attemptsMade >= (job.opts.attempts ?? 1)) {
await notifyTelegram(
`Order pipeline failed after ${job.attemptsMade} attempts\n` +
`Payment: ${job.data.paymentIntentId}\n` +
`Error: ${err.message}`
);
}
});









Step 3: Zoho CRM Integration



Zoho's API requires creating a contact and a deal separately. I batch this into one logical operation:




CODE
// lib/zoho.ts
interface ZohoOrderResult {
dealId: string;
contactId: string;
}

export async function createZohoDeal(order: Order): Promise<ZohoOrderResult> {
const token = await getZohoAccessToken(); // Handles OAuth token refresh

// Upsert the contact (search by email, create if not found)
const searchResponse = await fetch(
`https://www.zohoapis.eu/crm/v3/Contacts/search?criteria=(Email:equals:${encodeURIComponent(order.customerEmail)})`,
{ headers: { Authorization: `Zoho-oauthtoken ${token}` } }
);

let contactId: string;

if (searchResponse.ok) {
const existing = await searchResponse.json();
contactId = existing.data?.[0]?.id ?? (await createContact(order, token));
} else {
contactId = await createContact(order, token);
}

// Create the deal linked to the contact
const dealResponse = await fetch("https://www.zohoapis.eu/crm/v3/Deals", {
method: "POST",
headers: {
Authorization: `Zoho-oauthtoken ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: [
{
Deal_Name: `Order ${order.id}${order.customerName}`,
Stage: "Closed Won",
Amount: order.totalAmount / 100, // Stripe stores amounts in cents
Contact_Name: { id: contactId },
Description: order.lineItems.map((i) => `${i.name} × ${i.quantity}`).join("\n"),
Shipping_Address: order.shippingAddress,
},
],
}),
});

const deal = await dealResponse.json();
const dealId = deal.data[0].details.id;

return { dealId, contactId };
}






One gotcha: Zoho's EU data center uses zohoapis.eu, not zohoapis.com. Using the wrong domain produces auth errors that look like token problems.






Step 4: PostNord Shipment Creation



PostNord's API returns a base64-encoded PDF label along with the tracking number:




CODE
// lib/postnord.ts
interface ShipmentResult {
trackingNumber: string;
labelUrl: string; // S3 URL after uploading the label PDF
}

export async function createPostNordShipment(order: Order): Promise<ShipmentResult> {
const response = await fetch("https://api2.postnord.com/rest/shipment/v5/shipment", {
method: "POST",
headers: {
"x-api-key": process.env.POSTNORD_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
shipmentServiceCode: "19", // PostNord MyPack Home
sender: {
name: "Pikkuna Oy",
address1: process.env.SENDER_ADDRESS!,
city: process.env.SENDER_CITY!,
countryCode: "FI",
},
receiver: {
name: order.customerName,
address1: order.shippingAddress.line1,
city: order.shippingAddress.city,
postCode: order.shippingAddress.postalCode,
countryCode: order.shippingAddress.country,
email: order.customerEmail,
},
parcels: [{ weight: calculateTotalWeight(order.lineItems) }],
}),
});

const data = await response.json();
const shipment = data.CompositeShipmentData[0];
const trackingNumber = shipment.parcels[0].parcelNumber;

// Decode and upload the PDF label to S3 for permanent storage
const labelPdf = Buffer.from(shipment.pdfs[0].pdf, "base64");
const labelUrl = await uploadToS3(labelPdf, `labels/${trackingNumber}.pdf`);

return { trackingNumber, labelUrl };
}









Step 5: The Confirmation Email



The final step sends a transactional email via Mailgun. Templates are stored in Mailgun — this keeps HTML out of application code and lets non-developers edit copy:




CODE
// lib/mailgun.ts
import FormData from "form-data";
import Mailgun from "mailgun.js";

export async function sendConfirmationEmail(
order: Order,
{ trackingNumber, invoiceNumber }: { trackingNumber: string; invoiceNumber: string }
): Promise<void> {
const mg = new Mailgun(FormData).client({ key: process.env.MAILGUN_API_KEY! });

await mg.messages.create(process.env.MAILGUN_DOMAIN!, {
from: "Pikkuna Orders <[email protected]>",
to: order.customerEmail,
subject: `Your order is confirmed — tracking ${trackingNumber}`,
template: "order-confirmation",
"h:X-Mailgun-Variables": JSON.stringify({
customer_name: order.customerName.split(" ")[0],
tracking_number: trackingNumber,
tracking_url: `https://tracking.postnord.com/en/?id=${trackingNumber}`,
invoice_number: invoiceNumber,
order_items: order.lineItems,
locale: order.locale, // Customer's language — template is multilingual
}),
});
}









Handling Failures in the Pipeline



The question I get most often: what happens when one step fails?



Steps 1 and 2 (Zoho CRM and Airtable) are logging steps. If they fail, the customer is unaffected. BullMQ retries them, and if all retries are exhausted, Telegram gets an alert.



Steps 3 and 4 (PostNord and Netvisor) are more critical. If PostNord fails, there's no tracking number and no confirmation email. The worker retries with exponential backoff: 2s, 4s, 8s, 16s, 32s — 5 attempts total. PostNord has occasional outages; backoff handles the short ones automatically. If all 5 fail, a developer manually re-queues the job from the BullMQ dashboard.



One deliberate design choice: no rollbacks. If a Zoho deal is created but PostNord fails, I don't delete the Zoho deal. Partial state in the CRM is better than losing the data entirely. The Airtable row has a status field that tracks which pipeline steps completed — it serves as the source of truth.






Gotchas Nobody Warned Me About



Stripe retries webhooks for 72 hours. Your idempotency check must survive longer than that. A 24-hour Redis TTL is usually fine, but Redis can restart. For production I also store processed event IDs in the database as a permanent record, and use Redis as a fast first-check layer only.



Zoho rate limits the token endpoint at ~100 req/min. During flash sales, token refresh calls can hit this ceiling. Cache the access token and refresh only when expiry is imminent — not on every API call.



PostNord returns 200 OK for some error conditions. {"httpStatusCode": 200, "CompositeShipmentData": []} — an empty array with a success status — appears when a service code is unavailable for the destination country. Always check that CompositeShipmentData[0] exists and treat an empty array as a hard error.



request.arrayBuffer(), not request.json(). In Next.js App Router, parsing the body first corrupts the raw bytes that Stripe's signature verification needs. This trips up everyone migrating a Pages Router webhook to App Router.






Results



After deploying this pipeline at and . I'm available for projects and longer-term engagements.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Samsung Taps Mistral AI for On-Premises Chip Manufacturing
1 Quelle
CISA’s ChatGPT Incident Exposes a Bigger AI Governance Problem
1 Quelle
Beware — these new phishing attacks use a convincing fake Adobe Reader pages to trick victims into installing malware
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten E-commerce Order Automation: Stripe + Invoice + Shipping Workflow

Thematisch verwandte Begriffe: Ecommerce, Order, Automation, Stripe · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...