@trap_stevo/merchtide
v0.0.20
Published
Empowering the future of digital commerce, this API delivers the ultimate solution for building and managing online superstores. Seamlessly integrating product management, payment processing, and order tracking, it offers unmatched flexibility and scalabi
Maintainers
Keywords
Readme
🛍️ @trap_stevo/merchtide
Empower the future of digital commerce.
Unify product management, payment processing, and order tracking under a modular, event-driven API.
Build dynamic superstores, link products to multi-core payment systems, and sync customer activity in real time.
Craft frictionless storefronts, automate transactions, and merge every interaction between products, customers, and payments.
From microbrands to enterprise networks, this framework transforms commerce pipelines into streamlined digital ecosystems built for scale.
⚙️ Core Features
- 💳 Multi-Core Payments — Stripe & PayPal with instant switching
- 🧾 Order Orchestration — Create, track, update orders dynamically
- 🏷️ Product Management — Clean catalog API with processor registration
- 🔁 Subscriptions — Create, fetch, cancel (immediate or period-end), reactivate, refund
- 💼 Payment Methods — Tokenized creation, attach to customer, set default
- 💰 Refunds & Reactivations — One-line recovery or refund
- 🔔 Events — Lifecycle hooks for payments, refunds, subscriptions
- 🧠 Utilities — Monetary formatting, totals, normalization helpers
Design goals: minimal surface, explicit outcomes, configurable request payloads, and zero lock-in to a single processor.
🧠 System Requirements
| Requirement | Version | |---|---| | Node.js | ≥ 18.x | | npm | ≥ 9.x | | OS | Windows · macOS · Linux |
🧭 Quick Map
Payments → createPayment, invokePayment, createRefund
Subscriptions → createSubscription, cancelSubscription, reactivateSubscription, refundSubscription, getSubscriptionDetails
Customers → createCustomer, getCustomer, updateCustomer, deleteCustomer
Payment Methods → createPaymentMethod, attachPaymentMethodToCustomer, listCustomerPaymentMethods,
getCustomerDefaultPaymentMethod, containsCustomerPaymentMethod,
ensurePaymentMethodAttached, detachCustomerPaymentMethod, detachAllCustomerPaymentMethodsExcept,
selectCustomerPaymentMethod, getOrCreateAndAttachPaymentMethod,
setCustomerDefaultPaymentMethod, payWithTemporaryPaymentMethod, attachTemporaryPaymentMethod
Products → addProduct, getProductByID, listProducts, updateProduct, deleteProduct
Product↔Core → registerProductWithPayCore, updateProductPricingInPayCore
Orders → createOrder, getOrderByID, listOrders, updateOrder, updateOrderStatus, deleteOrder
Cores → initializePayCore, getPayCore
Events → "payment-created", "refund-created", "product-registered", "product-price-updated",
"subscription-created", "subscription-cancelled", "subscription-reactivated",
"subscription-refunded", "payment-method-attached", "payment-method-set-default"🏷️ Product Management
| Method | Description | Async |
|---|---|---|
| addProduct(product) | Add new product | ❌ |
| getProductByID(id) | Retrieve product by ID | ❌ |
| listProducts() | List products | ❌ |
| updateProduct(id, updates) | Update details | ❌ |
| deleteProduct(id) | Remove product | ❌ |
Example
const hoodie = merchTide.addProduct({
name : "Legendary Hoodie",
description : "Soft fabric · embroidered logo",
unitPrice : 5999 // cents
});💳 Register Product with a Pay Core
| Method | Description | Async |
|---|---|---|
| registerProductWithPayCore(core, productID, priceDetails, extra?) | Create/reuse processor product + price/plan | ✅ |
| updateProductPricingInPayCore(core, productID, newPriceDetails) | Roll forward price (Stripe) or update plan (PayPal) | ✅ |
priceDetails (Stripe)
{
"unitAmount": 4999,
"currency": "usd",
"recurring": { "interval": "month" } // optional
}priceDetails (PayPal)
{
"unitAmount": 4999,
"currency": "usd",
"recurring": { /* PayPal billing_cycles config */ }
}Example
await merchTide.registerProductWithPayCore("stripe", hoodie.id, {
unitAmount : 4999,
currency : "usd",
recurring : { interval : "month" }
}, {
// extra product fields for the processor product (e.g., tax_code, metadata)
});💳 Payments
| Method | Description | Async |
|---|---|---|
| createPayment(core, data, eventID?, eventMeta?, processedMeta?) | Creates a product-based or quantified payment intent (Stripe / PayPal) | ✅ |
| invokePayment(core, data, eventID?, eventMeta?, processedMeta?) | Charges an arbitrary total (no product quantity math) | ✅ |
Example – Stripe Purchase
await merchTide.createPayment("stripe", {
productID : hoodie.id,
quantity : 1,
payment_method : "pm_card_visa",
currencySymbol : "usd",
receiptEmail : "[email protected]",
automaticPaymentMethods : { enabled : true, allowRedirects : "never" },
requestOptions : { stripe : { idempotencyKey : "pay:ORD-991" } }
});Example – PayPal Arbitrary Payment
await merchTide.invokePayment("paypal", {
amount : 49.99,
currencySymbol : "USD",
payment_method : "PAYPAL-ALT",
receiptEmail : "[email protected]",
passthrough : { intent : "CAPTURE" }
});Refund Example
await merchTide.createRefund("stripe", {
charge : "ch_123",
amount : 1500,
reason : "requested_by_customer",
requestOptions : { idempotencyKey : "refund:RMA-991" }
});###💸 Payments — Configuration Matrix & Examples
Payment Data — Configuration Reference
Applies to
createPayment()andinvokePayment()(per core as supported).
| Key | Type | Purpose |
|---|---|---|
| productID | string | Product to derive unit price (createPayment) |
| quantity | number | Item count to bill |
| amount | number | Arbitrary amount (major units) for invokePayment |
| taxAmount | number | Additional charge (major units) |
| currencySymbol | string | "usd" / "USD" etc. |
| payment_method | string | "pm_..." (Stripe PM id) or raw card number in payment_method_data (see below) |
| paymentMethodType / type | string | Stripe PM type (default "card") for inline details |
| paymentMethodDataConfigurations | object | Extra keys merged into Stripe payment_method_data |
| billingAddress | object | { name, email, phone, line1, line2, city, state, postalCode, country } (Stripe) |
| automaticPaymentMethods | object | { enabled, allowRedirects, preferredMethods } (Stripe) |
| returnUrl | string | Post-confirmation/redirect target (e.g., 3DS) |
| metadata | object | Additional processor metadata |
| passthrough | object | Per-core payload additions { stripe : {...}, paypal : {...} } or shared {...} |
| override | boolean | If true, passthrough overrides the computed base payload |
| requestOptions | object | Per-core request opts { stripe : {...}, paypal : {...} } or shared {...} |
Inline Payment Method (Stripe)
If you don’t pass a"pm_*"inpayment_method, you can supply inline details:payment_method_data : { type : "card", card : { number, exp_month, exp_year, cvc } || { token : "tok_..." }, billing_details : { name, email, phone, address : { line1, line2, city, state, postal_code, country } } }
A) One-shot purchase from a product
await merchTide.createPayment("stripe", {
productID : hoodie.id,
quantity : 1,
payment_method : "pm_card_visa", // or raw card # via payment_method_data; see below
currencySymbol : "usd",
receiptEmail : "[email protected]",
// --- Optional Price Adders ---
taxAmount : 0, // in major units (e.g., 1.99 → $1.99)
// --- Optional Intent Features (Stripe) ---
automaticPaymentMethods : {
enabled : true,
allowRedirects : "never",
preferredMethods: [/* e.g., "card", "us_bank_account" */]
},
returnUrl : "https://example.com/checkout/return",
// --- Payload shaping ---
passthrough : { stripe : { metadata : { orderId : "A123" } }, paypal : { custom : "A123" } },
override : false, // if true: passthrough overrides base intent body
requestOptions: { stripe : { idempotencyKey : "createPayment:A123" } }
});B) Arbitrary amount (no product quantity math)
await merchTide.invokePayment("paypal", {
amount : 49.99, // major units
taxAmount : 0, // major units
currencySymbol : "USD",
payment_method : "PAYPAL-ALT", // see notes below
receiptEmail : "[email protected]",
passthrough : { intent : "CAPTURE" },
override : false,
requestOptions : { /* PayPal SDK request opts */ }
}, "payment-created");👥 Customers
| Method | Description | Async |
|---|---|---|
| createCustomer(core, data) | Create a new customer in the selected core | ✅ |
| getCustomer(core, customerID) | Retrieve customer details by ID | ✅ |
| updateCustomer(core, customerID, updates) | Update metadata / invoice settings | ✅ |
| deleteCustomer(core, customerID) | Delete a customer (core-specific) | ✅ |
Example
// Create
const customer = await merchTide.createCustomer("stripe", {
name : "Jane Doe",
email : "[email protected]"
});
// Update
await merchTide.updateCustomer("stripe", customer.id, {
metadata : { tier : "gold", locale : "en-US" }
});
// Retrieve
const current = await merchTide.getCustomer("stripe", customer.id);
// Delete
await merchTide.deleteCustomer("stripe", customer.id);Notes
- Customers are validated before subscription or payment methods operations.
- All customer actions route through initialized payment cores (Stripe / PayPal).
💼 Payment Methods
| Method | Description | Async |
|---|---|---|
| createPaymentMethod(core, data) | Create PM via token or raw card | ✅ |
| attachPaymentMethodToCustomer(core, customerID, methodID) | Attach & set default invoice PM | ✅ |
| listCustomerPaymentMethods(core, customerID, type?) | List customer PMs (default "card") | ✅ |
| getCustomerDefaultPaymentMethod(core, customerID) | Fetch default PM | ✅ |
| containsCustomerPaymentMethod(core, { customerID, methodID }) | Check ownership | ✅ |
| ensurePaymentMethodAttached(core, { customerID, methodID, setAsDefault }) | Attach if missing (+optional default) | ✅ |
| detachCustomerPaymentMethod(core, customerID, methodID) | Safe detach (skips default) | ✅ |
| detachAllCustomerPaymentMethodsExcept(core, customerID, keepID) | Bulk detach by list | ✅ |
| selectCustomerPaymentMethod(core, { customerID, strategy }) | Pick "default" | "latest" | ✅ |
| getOrCreateAndAttachPaymentMethod(...) | Create from token/data, attach, optionally default | ✅ |
| setCustomerDefaultPaymentMethod(...) | Idempotent defaulting + optional pruning | ✅ |
| payWithTemporaryPaymentMethod(...) | One-off payment with ephemeral PM (auto-detach) | ✅ |
| attachTemporaryPaymentMethod(...) | Attach ephemeral PM, perform operation, auto-detach | ✅ |
Create → Attach → Default
const customer = await merchTide.createCustomer("stripe", { name : "Jane", email : "[email protected]" });
const pm = await merchTide.createPaymentMethod("stripe", {
type : "card",
token : "tok_visa",
billingDetails : { name : "Jane Doe", email : "[email protected]" }
});
await merchTide.attachPaymentMethodToCustomer("stripe", customer.id, pm.id);
await merchTide.setCustomerDefaultPaymentMethod("stripe", {
customerID : customer.id,
methodID : pm.id,
detachOthers : true
});Ephemeral (take payment, then clean up)
const result = await merchTide.attachTemporaryPaymentMethod("stripe", {
customerID : customer.id,
token : "tok_visa",
perform : {
type : "createPayment",
args : { productID : hoodie.id, quantity : 1, currencySymbol : "usd", receiptEmail : "[email protected]" }
},
autoDetach : true
});🔁 Subscriptions — Lifecycle & Options
| Method | Purpose | Async |
|---|---|---|
| createSubscription(core, data) | Start recurring billing | ✅ |
| getSubscriptionDetails(core, id) | Fetch details | ✅ |
| cancelSubscription(core, id, options?) | Immediate or period-end (Stripe) | ✅ |
| reactivateSubscription(core, id) | Resume if delayed-cancel or suspended | ✅ |
createSubscription (Stripe options)
| Key | Type | Notes |
|---|---|---|
| customerID | string | Required |
| productID | priceID | string | Product+registered price or direct price id |
| startNow | boolean | If true, trial_end: "now" |
| trialEnd | number (unix) | Future start |
| collection_method | string | "charge_automatically" | "send_invoice" |
| payment_behavior | string | Stripe behavior for invoice/payment creation |
| proration_behavior | string | "create_prorations" | "none" |
| default_payment_method | paymentMethodID | string | Will be attached if not owned by customer |
| passthrough, override, requestOptions | object | See Payments table |
createSubscription (PayPal options)
| Key | Type | Notes |
|---|---|---|
| plan_id | priceID | string | PayPal plan id |
| productID | string | If registered with paypalPlanID / paypalPriceID |
| customerEmail | string | Optional |
| customerFirstName / customerLastName | string | Optional |
| trialEnd | number (unix) | Schedules start_time |
| passthrough, override, requestOptions | object | See Payments table |
Cancel / Reactivate / Refund
// Cancel at period end (Stripe)
await merchTide.cancelSubscription("stripe", "sub_123", { at_period_end : true });
// Immediate cancel
await merchTide.cancelSubscription("stripe", "sub_456");
// Reactivate (e.g., previously set to cancel at period end)
await merchTide.reactivateSubscription("stripe", "sub_123");
// Refund latest invoice (Stripe) — partial $19.99
await merchTide.refundSubscription("stripe", "sub_123", 1999);Note: Stripe supports end-of-period cancellation (
{ at_period_end : true }).
PayPal supports immediate cancellation; reactivation uses status-specific actions.
💰 Refunds (Standalone)
| Method | Description | Async |
|---|---|---|
| createRefund(core, data) | Create a standalone refund for a charge or transaction (Stripe / PayPal) | ✅ |
| refundSubscription(core, subscriptionID, amount?, config?) | Refund latest invoice (Stripe) or last payment (PayPal) | ✅ |
await merchTide.createRefund("stripe", {
charge : "ch_123",
amount : 1500, // cents
reason : "requested_by_customer",
passthrough : { metadata : { caseId : "RMA-991" } },
override : false,
requestOptions: { idempotencyKey : "refund:RMA-991" }
});await merchTide.createRefund("paypal", {
transactionID : "9AB12345CD6789012",
amount : 15.00, // major units
currencySymbol: "USD"
});🧾 Order Management
| Method | Description | Async |
|---|---|---|
| createOrder(order) | Create new order | ❌ |
| getOrderByID(id) | Retrieve order by ID | ❌ |
| listOrders() | List orders | ❌ |
| updateOrder(id, updates) | Update order fields | ❌ |
| updateOrderStatus(id, status) | Change status | ❌ |
| deleteOrder(id) | Delete order | ❌ |
🔔 Events
| Event | When | Payload (shape) |
|---|---|---|
| product-registered | product ↔ core registration | { ...updatedProduct } |
| product-price-updated | on price roll-forward | { ...updatedProduct } |
| payment-created | after payment intent/order | { coreName, paymentData, result, subtotalPaid, totalPaid, taxPaid } |
| refund-created | after refund | { coreName, data, result } |
| subscription-created | after subscription create | { coreName, data, result } |
| subscription-cancelled | after cancel | { coreName, subscriptionID, result, options } |
| subscription-reactivated | after resume | { coreName, subscription } |
| subscription-refunded | after subscription refund | { coreName, subscriptionID, result } |
| payment-method-attached | on PM attach | { coreName, customerID, methodID, created?, attached? } |
| payment-method-set-default | on default set | { coreName, customerID, methodID } |
Example
merchTide.on("subscription-created", ({ coreName, data }) => {
console.log("✨ New subscription via", coreName, "for", data.customerID);
});🎯 Bidding / Auctions (Event-Driven Pattern)
Bidding rides on the library’s event bus and payment/order surfaces.
You define your own auction models and emit events; then settle with invokePayment or createPayment, and update orders atomically.
Why this pattern? It keeps the auction logic domain-owned while leveraging robust, idempotent payment + refund + order utilities.
Event Contracts (you emit / listen)
| Event | When | Payload (shape) |
|---|---|---|
| bid-placed | A user places a bid | { auctionID, userID, amount, currency, timestamp } |
| bid-outbid | A higher bid replaces another | { auctionID, previousBid, newBid } |
| bid-won | Auction closes | { auctionID, winningBid, productID } |
| bid-settled | Payment completes | { auctionID, orderID, paymentResult } |
| bid-failed | Payment/settlement failed | { auctionID, orderID, error } |
Payload schema (reference):
{
"Bid": {
"auctionID": "string",
"userID": "string",
"amount": "number (major units, e.g., 49.99)",
"currency": "string (e.g., 'usd' or 'USD')",
"timestamp": "number (Date.now())"
},
"WinningBid": {
"auctionID": "string",
"productID": "string",
"userID": "string",
"amount": "number",
"currency": "string",
"payment_method": "string (e.g., 'pm_...' or PSP alias)",
"metadata": "object (optional)"
}
}Recommended flow
- User places a bid → validate → emit
bid-placed. - Outbid event (optional) → emit
bid-outbid. - Auction close → determine winner → emit
bid-won. - Settlement → charge winner via
invokePayment/createPayment→ emitbid-settled(orbid-failed) → update order status.
Place a bid
function placeBid(merchTide, { auctionID, userID, amount, currency = "usd" }) {
if (!(auctionID && userID) || !(amount > 0)) {
throw new Error("Invalid bid payload.");
}
const bid = {
auctionID,
userID,
amount,
currency,
timestamp : Date.now()
};
merchTide.emit("bid-placed", bid);
return bid;
}Anti-sniping (optional)
Extend deadline when a last-minute bid lands:
function maybeExtendAuction({ auction, lastBidAt, windowMs = 15_000 }) {
const now = Date.now();
if (auction.endsAt - now <= windowMs) {
auction.endsAt = now + windowMs;
return true;
}
return false;
}Settlement (Stripe primary → PayPal fallback)
Use arbitrary-amount flow (invokePayment) to charge the winning amount, while keeping order status consistent.
Supports passthrough, override, and requestOptions (idempotency).
async function settleWinningBid(merchTide, {
auctionID,
productID,
winningBid, // { userID, amount, currency, payment_method?, metadata? }
orderID, // your order id
receiptEmail
}) {
try {
// Primary: Stripe
const result = await merchTide.invokePayment("stripe", {
amount : winningBid.amount,
currencySymbol : winningBid.currency,
productID,
payment_method : winningBid.payment_method || "pm_card_visa",
// Attach business metadata safely (won’t expose internals)
passthrough : { stripe : { metadata : {
auctionID,
orderID,
winner : winningBid.userID
}}},
// Optional idempotency
requestOptions : { stripe : { idempotencyKey : `bid:${auctionID}:order:${orderID}` } }
}, "bid-settled", { auctionID, orderID, winner : winningBid.userID });
merchTide.updateOrderStatus(orderID, "paid-stripe");
merchTide.emit("bid-settled", { auctionID, orderID, paymentResult : result });
return result;
}
catch (err) {
// Fallback: PayPal
try {
const result = await merchTide.invokePayment("paypal", {
amount : winningBid.amount,
currencySymbol : (winningBid.currency || "USD").toUpperCase(),
productID,
payment_method : "PAYPAL-ALT",
receiptEmail,
passthrough : { paypal : { custom_id : orderID } },
requestOptions : { /* PayPal SDK options if needed */ }
}, "bid-settled", { auctionID, orderID, winner : winningBid.userID });
merchTide.updateOrderStatus(orderID, "paid-paypal");
merchTide.emit("bid-settled", { auctionID, orderID, paymentResult : result });
return result;
} catch (fallbackErr) {
merchTide.updateOrderStatus(orderID, "payment-failed");
merchTide.emit("bid-failed", { auctionID, orderID, error : String(fallbackErr?.message || fallbackErr) });
throw fallbackErr;
}
}
}Outbid signalling (optional)
function notifyOutbid(merchTide, { auctionID, previousBid, newBid }) {
merchTide.emit("bid-outbid", { auctionID, previousBid, newBid });
}Refund policy (optional)
If a post-settlement dispute occurs, use normal refunds:
await merchTide.createRefund("stripe", {
charge : "ch_123", // or use subscription refund APIs if recurring
amount : 2500, // cents
requestOptions : { idempotencyKey : `refund:${auctionID}:${orderID}` }
});Event listeners (analytics, emails, ledger)
merchTide.on("bid-placed", (b) => {
// analytics.track("BidPlaced", b);
});
merchTide.on("bid-settled", ({ auctionID, orderID, paymentResult }) => {
// sendReceiptEmail(...), audit ledger, release item, etc.
});
merchTide.on("bid-failed", ({ auctionID, orderID, error }) => {
// alert ops / notify winner to retry
});Notes & best practices
- Use idempotency keys on settlement to prevent duplicate charges.
- Prefer
invokePaymentfor winner-pays scenarios (precise arbitrary totals). - Use
passthrough+overrideto enrich gateway payloads without exposing internals. - Keep bid state in your own storage; use events solely for workflow + side-effects.
- Emit
bid-wonbefore charging if you need auditability of the decision boundary; otherwise emit after successful capture.
🌐 Dual-Core Checkout (Stripe → PayPal Fallback)
async function handleCheckout(order) {
try {
await merchTide.createPayment("stripe", {
productID : order.productID,
quantity : order.quantity,
payment_method : "pm_card_visa",
currencySymbol : "usd",
receiptEmail : order.email
});
merchTide.updateOrderStatus(order.id, "paid-stripe");
} catch (err) {
await merchTide.invokePayment("paypal", {
amount : order.total,
currencySymbol : "USD",
payment_method : "PAYPAL-ALT",
receiptEmail : order.email
});
merchTide.updateOrderStatus(order.id, "paid-paypal");
}
}🌐 Webhook Synchronization (Unified Live Updates)
import express from "express";
import bodyParser from "body-parser";
import stripeModule from "stripe";
import paypal from "paypal-rest-sdk";
import MerchTide from "@trap_stevo/merchtide";
const stripe = stripeModule(process.env.STRIPE_SECRET_KEY);
const app = express();
app.use(bodyParser.raw({ type : "application/json" }));
app.post("/webhook", async (req, res) => {
const rawBody = req.body;
const headers = req.headers;
try {
if (headers["stripe-signature"]) {
const event = stripe.webhooks.constructEvent(
rawBody,
headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
MerchTide.emit("stripe-webhook", event);
return res.status(200).send("Stripe webhook processed");
}
if (headers["paypal-transmission-sig"]) {
paypal.notification.webhookEvent.verify(
headers,
rawBody.toString(),
process.env.PAYPAL_WEBHOOK_ID,
(error, response) => {
if (response && response.verification_status === "SUCCESS") {
const event = JSON.parse(rawBody);
MerchTide.emit("paypal-webhook", event);
res.status(200).send("PayPal webhook processed");
} else {
res.status(400).send("Invalid PayPal signature");
}
}
);
return;
}
res.status(400).send("No recognized webhook source");
} catch (err) {
res.status(400).send("Invalid signature");
}
});🧩 Configuration Patterns
1) Passthrough vs Override
passthroughlets you add processor-specific keys to the final request body:- Shape can be shared (
{ key: value }) or per-core ({ stripe : {...}, paypal : {...} }).
- Shape can be shared (
override: truemakes your passthrough win when keys collide with the computed base body.
// Add Stripe metadata and force it to override base fields if any conflict
await merchTide.createPayment("stripe", {
productID : hoodie.id,
quantity : 2,
passthrough: { stripe : { metadata : { campaign : "Q4-HOLIDAY" } } },
override : true
});2) Request Options (Idempotency, headers, etc.)
Supply per-core requestOptions when you need idempotency keys, custom headers, or advanced SDK settings.
await merchTide.createRefund("stripe", {
charge : "ch_123",
amount : 500
}, /* eventID */ undefined, /* eventMeta */ undefined, /* resultMeta */ undefined);
// or directly via requestOptions:
await merchTide.createPayment("stripe", {
productID : hoodie.id,
quantity : 1,
requestOptions : { stripe : { idempotencyKey : "pay:order-991" } }
});3) Automatic Payment Methods (Stripe)
automaticPaymentMethods : {
enabled : true,
allowRedirects : "never", // "always" | "required"
preferredMethods: ["card", "us_bank_account"]
}4) Inline Billing + PM data (Stripe)
payment_method_data : {
type : "card",
card : { token : "tok_visa" }, // or number/exp/cvc
billing_details : {
name : "Jane Doe",
email : "[email protected]",
phone : "+1-555-555-5555",
address: {
line1 : "123 Main St",
line2 : "",
city : "Austin",
state : "TX",
postal_code : "73301",
country : "US"
}
}
}5) Subscription Start Control
startNow : true→trial_end: "now"(Stripe)trialEnd : unix→ Defer start- PayPal uses
start_timeISO for deferrals.
✅ Usage Checklist
- Initialize both cores you plan to use.
- Register products with each core to obtain price/plan IDs if you need recurring billing.
- Choose
createPaymentfor itemized purchases orinvokePaymentfor arbitrary amounts. - Use events to sync your state machine (order status, customer ledger, analytics).
- Prefer idempotency keys on payment/refund/subscribe flows.
🧯 Common Errors & Remedies
| Symptom | Why | Fix |
|---|---|---|
| “Product not found or missing unit price” | createPayment expects catalog item | Use invokePayment or add unitPrice to catalog |
| “Payment core 'x' not initialized” | Core not initializePayCore(...) | Initialize before use |
| “Invalid amount / Tax exceeds total payment” | Math guardrails | Check amount, taxAmount, currencySymbol |
| “No invoice/charge found for subscription” | Refund flow requires last invoice | Ensure last invoice exists or use partial refund rules |
| “No such customer / test vs live” | Mode mismatch or wrong ID | Re-create in right mode; verify keys |
📦 Installation
npm install @trap_stevo/merchtide⚡ Quick Start
const MerchTide = require("@trap_stevo/merchtide");
const merchTide = new MerchTide();
await merchTide.initializePayCore("stripe", { apiKey : process.env.STRIPE_SECRET_KEY });
await merchTide.initializePayCore("paypal", {
client_id : process.env.PAYPAL_CLIENT_ID,
client_secret : process.env.PAYPAL_CLIENT_SECRET,
mode : "sandbox"
});
const hoodie = merchTide.addProduct({ name : "Midnight Hoodie", unitPrice : 4999 });
const order = merchTide.createOrder({
productID : hoodie.id,
quantity : 1,
customer : "[email protected]"
});
const stripe = merchTide.getPayCore("stripe");
const customer = await merchTide.createCustomer("stripe", { name : "Trial User", email : "[email protected]" });
const list = await stripe.paymentMethods.list({ customer : customer.id, type : "card" });
const pmID = list.data[0]?.id || (await merchTide.createPaymentMethod("stripe", { type : "card", card : { token : "tok_visa" } })).id;
await merchTide.attachPaymentMethodToCustomer("stripe", customer.id, pmID);
await merchTide.createSubscription("stripe", {
customerID : customer.id,
productID : hoodie.id,
trialEnd : Math.floor(Date.now() / 1000) + 7 * 86400
});
await merchTide.cancelSubscription("stripe", "sub_123", { at_period_end : true });🧪 End-to-End Example
const MerchTide = require("@trap_stevo/merchtide");
const merchTide = new MerchTide();
await merchTide.initializePayCore("stripe", { apiKey : process.env.STRIPE_SECRET_KEY });
await merchTide.initializePayCore("paypal", {
client_id : process.env.PAYPAL_CLIENT_ID,
client_secret : process.env.PAYPAL_CLIENT_SECRET,
mode : "sandbox"
});
const hoodie = merchTide.addProduct({ name : "Midnight Hoodie", unitPrice : 4999 });
const order = merchTide.createOrder({
id : "ORD-1001",
productID : hoodie.id,
quantity : 1,
customer : "[email protected]"
});
// Customer + PM
const customer = await merchTide.createCustomer("stripe", { name : "Trial User", email : "[email protected]" });
const pmID = await merchTide.getOrCreateAndAttachPaymentMethod("stripe", {
customerID : customer.id,
token : "tok_visa",
setAsDefault : true
});
// Subscription with 7-day trial
await merchTide.registerProductWithPayCore("stripe", hoodie.id, {
unitAmount : 4999,
currency : "usd",
recurring : { interval : "month" }
});
await merchTide.createSubscription("stripe", {
customerID : customer.id,
productID : hoodie.id,
trialEnd : Math.floor(Date.now() / 1000) + 7 * 86400,
requestOptions : { stripe : { idempotencyKey : "sub:ORD-1001" } }
});
// One-off purchase fallback to PayPal
try {
await merchTide.createPayment("stripe", {
productID : hoodie.id,
quantity : 1,
payment_method : pmID,
currencySymbol : "usd",
receiptEmail : "[email protected]"
});
merchTide.updateOrderStatus(order.id, "paid-stripe");
} catch {
await merchTide.invokePayment("paypal", {
amount : 49.99,
currencySymbol : "USD",
payment_method : "PAYPAL-ALT",
receiptEmail : "[email protected]"
});
merchTide.updateOrderStatus(order.id, "paid-paypal");
}📜 License
See License in LICENSE.md
🌊 Power Commerce. Flow Effortlessly.
Unify the tides of digital commerce — orchestrating payments, subscriptions, and products with seamless synchronization and elegance.
From indie creators to enterprise networks, make transactions flow like water — secure, adaptive, and built for the future.
