Trifecta-metoden: Hur vidarebefordra e-post byggde ett skottsäkert betalningssystem med Stripe och PayPal

Lär dig hur vårt utvecklingsteam integrerade både Stripe och PayPal med en trifecta-metod som säkerställer 1:1 realtidsnoggrannhet i hela vårt system.

Förord

På Forward Email har vi alltid prioriterat att skapa system som är pålitliga, exakta och användarvänliga. När det kom till att implementera vårt betalningshanteringssystem visste vi att vi behövde en lösning som kunde hantera flera betalningsprocessorer samtidigt som vi bibehöll perfekt datakonsistens. Det här blogginlägget beskriver hur vårt utvecklingsteam integrerade både Stripe och PayPal med en trifecta-metod som säkerställer 1:1 realtidsnoggrannhet över hela vårt system.

Utmaningen: Flera betalningsbehandlare, en källa till sanning

Som en integritetsfokuserad e-posttjänst ville vi ge våra användare betalningsalternativ. Vissa föredrar enkelheten med kreditkortsbetalningar genom Stripe, medan andra värdesätter det extra skiktet av separation som PayPal tillhandahåller. Men att stödja flera betalningsprocessorer introducerar betydande komplexitet:

  1. Hur säkerställer vi konsekventa data över olika betalningssystem?
  2. Hur hanterar vi edge-ärenden som tvister, återbetalningar eller misslyckade betalningar?
  3. Hur upprätthåller vi en enda källa till sanning i vår databas?

Vår lösning var att implementera det vi kallar "trifecta approach" - ett treskiktssystem som ger redundans och säkerställer datakonsistens oavsett vad som händer.

Trifecta-metoden: tre lager av tillförlitlighet

Vårt betalningssystem består av tre kritiska komponenter som arbetar tillsammans för att säkerställa perfekt datasynkronisering:

  1. Omdirigeringar efter kassan - Fånga betalningsinformation direkt efter kassan
  2. Webhook-hanterare - Bearbetning av händelser i realtid från betalningsprocessorer
  3. Automatiserade jobb - Regelbundet verifiera och stämma av betalningsdata

Låt oss dyka in i varje komponent och se hur de fungerar tillsammans.

flowchart TD
    User([User]) --> |Selects plan| Checkout[Checkout Page]
%% Layer 1: Post-checkout redirects
subgraph "Layer 1: Post-checkout Redirects"
    Checkout --> |Credit Card| Stripe[Stripe Checkout]
    Checkout --> |PayPal| PayPal[PayPal Payment]

    Stripe --> |Success URL with session_id| SuccessPage[Success Page]
    PayPal --> |Return URL| SuccessPage

    SuccessPage --> |Verify payment| Database[(Database Update)]
end

%% Layer 2: Webhooks
subgraph "Layer 2: Webhook Handlers"
    StripeEvents[Stripe Events] --> |Real-time notifications| StripeWebhook[Stripe Webhook Handler]
    PayPalEvents[PayPal Events] --> |Real-time notifications| PayPalWebhook[PayPal Webhook Handler]

    StripeWebhook --> |Verify signature| ProcessStripeEvent[Process Stripe Event]
    PayPalWebhook --> |Verify signature| ProcessPayPalEvent[Process PayPal Event]

    ProcessStripeEvent --> Database
    ProcessPayPalEvent --> Database
end

%% Layer 3: Automated jobs
subgraph "Layer 3: Bree Automated Jobs"
    BreeScheduler[Bree Scheduler] --> StripeSync[Stripe Sync Job]
    BreeScheduler --> PayPalSync[PayPal Sync Job]
    BreeScheduler --> AccuracyCheck[Subscription Accuracy Check]

    StripeSync --> |Verify & reconcile| Database
    PayPalSync --> |Verify & reconcile| Database
    AccuracyCheck --> |Ensure consistency| Database
end

%% Edge cases
subgraph "Edge Case Handling"
    ProcessStripeEvent --> |Fraud detection| FraudCheck[Fraud Check]
    ProcessPayPalEvent --> |Dispute created| DisputeHandler[Dispute Handler]

    FraudCheck --> |Ban user if fraudulent| Database
    DisputeHandler --> |Accept claim & refund| Database

    FraudCheck --> |Send alert| AdminNotification[Admin Notification]
    DisputeHandler --> |Send alert| AdminNotification
end

%% Style definitions
classDef primary fill:blue,stroke:#333,stroke-width:2px;
classDef secondary fill:red,stroke:#333,stroke-width:1px;
classDef tertiary fill:green,stroke:#333,stroke-width:1px;

class Checkout,SuccessPage primary;
class Stripe,PayPal,StripeWebhook,PayPalWebhook,BreeScheduler secondary;
class FraudCheck,DisputeHandler tertiary;

Lager 1: Omdirigeringar efter utcheckning

Det första lagret av vår trifecta-metod sker direkt efter att en användare har slutfört en betalning. Både Stripe och PayPal tillhandahåller mekanismer för att omdirigera användare tillbaka till vår webbplats med transaktionsinformation.

Implementering av Stripe Checkout

För Stripe använder vi deras Checkout Sessions API för att skapa en sömlös betalningsupplevelse. När en användare väljer en plan och väljer att betala med ett kreditkort skapar vi en utcheckningssession med specifik framgång och avbryter webbadresser:

const options = {
  mode: paymentType === 'one-time' ? 'payment' : 'subscription',
  customer: ctx.state.user[config.userFields.stripeCustomerID],
  client_reference_id: reference,
  metadata: {
    plan
  },
  line_items: [
    {
      price,
      quantity: 1,
      description
    }
  ],
  locale: config.STRIPE_LOCALES.has(ctx.locale) ? ctx.locale : 'auto',
  cancel_url: `${config.urls.web}${ctx.path}${
    isMakePayment || isEnableAutoRenew ? '' : `/?plan=${plan}`
  }`,
  success_url: `${config.urls.web}${ctx.path}/?${
    isMakePayment || isEnableAutoRenew ? '' : `plan=${plan}&`
  }session_id={CHECKOUT_SESSION_ID}`,
  allow_promotion_codes: true
};

// Create the checkout session and redirect const session = await stripe.checkout.sessions.create(options); const redirectTo = session.url; if (ctx.accepts('html')) { ctx.status = 303; ctx.redirect(redirectTo); } else { ctx.body = { redirectTo }; }

Den kritiska delen här är success_url parameter, som inkluderar session_id som en frågeparameter. När Stripe omdirigerar användaren tillbaka till vår sida efter en lyckad betalning kan vi använda detta sessions-ID för att verifiera transaktionen och uppdatera vår databas därefter.

PayPal-betalningsflöde

För PayPal använder vi ett liknande tillvägagångssätt med deras Orders API:

const requestBody = {
  intent: 'CAPTURE',
  application_context: {
    cancel_url: `${config.urls.web}${ctx.path}${
      isMakePayment || isEnableAutoRenew ? '' : `/?plan=${plan}`
    }`,
    return_url: `${config.urls.web}${ctx.path}/?plan=${plan}`,
    brand_name: 'Forward Email',
    shipping_preference: 'NO_SHIPPING',
    user_action: 'PAY_NOW'
  },
  payer: {
    email_address: ctx.state.user.email
  },
  purchase_units: [
    {
      reference_id: ctx.state.user.id,
      description,
      custom_id: sku,
      invoice_id: reference,
      soft_descriptor: sku,
      amount: {
        currency_code: 'USD',
        value: price,
        breakdown: {
          item_total: {
            currency_code: 'USD',
            value: price
          }
        }
      },
      items: [
        {
          name,
          description,
          sku,
          unit_amount: {
            currency_code: 'USD',
            value: price
          },
          quantity: '1',
          category: 'DIGITAL_GOODS'
        }
      ]
    }
  ]
};

I likhet med Stripe, specificerar vi return_url och cancel_url parametrar för att hantera omdirigeringar efter betalning. När PayPal omdirigerar användaren tillbaka till vår webbplats kan vi fånga betalningsinformationen och uppdatera vår databas.

sequenceDiagram
    participant User
    participant FE as Forward Email
    participant Stripe
    participant PayPal
    participant DB as Database
    participant Bree as Bree Job Scheduler
%% Initial checkout flow
User->>FE: Select plan & payment method

alt Credit Card Payment
    FE->>Stripe: Create Checkout Session
    Stripe-->>FE: Return session URL
    FE->>User: Redirect to Stripe Checkout
    User->>Stripe: Complete payment
    Stripe->>User: Redirect to success URL with session_id
    User->>FE: Return to success page
    FE->>Stripe: Verify session using session_id
    Stripe-->>FE: Return session details
    FE->>DB: Update user plan & payment status
else PayPal Payment
    FE->>PayPal: Create Order
    PayPal-->>FE: Return approval URL
    FE->>User: Redirect to PayPal
    User->>PayPal: Approve payment
    PayPal->>User: Redirect to return URL
    User->>FE: Return to success page
    FE->>PayPal: Capture payment
    PayPal-->>FE: Return payment details
    FE->>DB: Update user plan & payment status
end

%% Webhook flow (asynchronous)
Note over Stripe,PayPal: Payment events occur (async)

alt Stripe Webhook
    Stripe->>FE: Send event notification
    FE->>FE: Verify webhook signature
    FE->>DB: Process event & update data
    FE-->>Stripe: Acknowledge receipt (200 OK)
else PayPal Webhook
    PayPal->>FE: Send event notification
    FE->>FE: Verify webhook signature
    FE->>DB: Process event & update data
    FE-->>PayPal: Acknowledge receipt (200 OK)
end

%% Bree automated jobs
Note over Bree: Scheduled jobs run periodically

Bree->>Stripe: Get all customers & subscriptions
Stripe-->>Bree: Return customer data
Bree->>DB: Compare & reconcile data

Bree->>PayPal: Get all subscriptions & transactions
PayPal-->>Bree: Return subscription data
Bree->>DB: Compare & reconcile data

%% Edge case: Dispute handling
Note over User,PayPal: User disputes a charge

PayPal->>FE: DISPUTE.CREATED webhook
FE->>PayPal: Accept claim automatically
FE->>DB: Update user status
FE->>User: Send notification email

Lager 2: Webhook-hanterare med signaturverifiering

Även om omdirigeringar efter utcheckning fungerar bra för de flesta scenarier, är de inte idiotsäkra. Användare kan stänga sin webbläsare innan de omdirigeras, eller nätverksproblem kan hindra omdirigeringen från att slutföras. Det är där webhooks kommer in.

Både Stripe och PayPal tillhandahåller webhook-system som skickar realtidsmeddelanden om betalningshändelser. Vi har implementerat robusta webhook-hanterare som verifierar äktheten av dessa meddelanden och behandlar dem därefter.

Implementering av Stripe Webhook

Vår Stripe webhook-hanterare verifierar signaturen för inkommande webhook-händelser för att säkerställa att de är legitima:

async function webhook(ctx) {
  const sig = ctx.request.get('stripe-signature');
  // throw an error if something was wrong
  if (!isSANB(sig))
    throw Boom.badRequest(ctx.translateError('INVALID_STRIPE_SIGNATURE'));
  const event = stripe.webhooks.constructEvent(
    ctx.request.rawBody,
    sig,
    env.STRIPE_ENDPOINT_SECRET
  );
  // throw an error if something was wrong
  if (!event)
    throw Boom.badRequest(ctx.translateError('INVALID_STRIPE_SIGNATURE'));
  ctx.logger.info('stripe webhook', { event });
  // return a response to acknowledge receipt of the event
  ctx.body = { received: true };
  // run in background
  processEvent(ctx, event)
    .then()
    .catch((err) => {
      ctx.logger.fatal(err, { event });
      // email admin errors
      emailHelper({
        template: 'alert',
        message: {
          to: config.email.message.from,
          subject: `Error with Stripe Webhook (Event ID ${event.id})`
        },
        locals: {
          message: `<pre><code>${safeStringify(
            parseErr(err),
            null,
            2
          )}</code></pre>`
        }
      })
        .then()
        .catch((err) => ctx.logger.fatal(err, { event }));
    });
}

De stripe.webhooks.constructEvent funktionen verifierar signaturen med vår slutpunktshemlighet. Om signaturen är giltig behandlar vi händelsen asynkront för att undvika att blockera webhook-svaret.

PayPal Webhook-implementering

På samma sätt verifierar vår PayPal webhook-hanterare äktheten av inkommande aviseringar:

async function webhook(ctx) {
  const response = await promisify(
    paypal.notification.webhookEvent.verify,
    paypal.notification.webhookEvent
  )(ctx.request.headers, ctx.request.body, env.PAYPAL_WEBHOOK_ID);
  // throw an error if something was wrong
  if (!_.isObject(response) || response.verification_status !== 'SUCCESS')
    throw Boom.badRequest(ctx.translateError('INVALID_PAYPAL_SIGNATURE'));
  // return a response to acknowledge receipt of the event
  ctx.body = { received: true };
  // run in background
  processEvent(ctx)
    .then()
    .catch((err) => {
      ctx.logger.fatal(err);
      // email admin errors
      emailHelper({
        template: 'alert',
        message: {
          to: config.email.message.from,
          subject: `Error with PayPal Webhook (Event ID ${ctx.request.body.id})`
        },
        locals: {
          message: `<pre><code>${safeStringify(
            parseErr(err),
            null,
            2
          )}</code></pre>`
        }
      })
        .then()
        .catch((err) => ctx.logger.fatal(err));
    });
}

Båda webhook-hanterarna följer samma mönster: verifiera signaturen, bekräfta mottagandet och bearbeta händelsen asynkront. Detta säkerställer att vi aldrig missar en betalningshändelse, även om omdirigeringen efter utcheckningen misslyckas.

Lager 3: Automatiserade jobb med Bree

Det sista lagret av vår trifecta-metod är en uppsättning automatiserade jobb som regelbundet verifierar och stämmer av betalningsdata. Vi använder Bree, en jobbschemaläggare för Node.js, för att köra dessa jobb med jämna mellanrum.

Prenumerationsprenumerationskontroll

Ett av våra nyckeljobb är prenumerationsprenumerationskontrollen, som säkerställer att vår databas exakt återspeglar prenumerationsstatusen i Stripe:

async function mapper(customer) {
  // wait a second to prevent rate limitation error
  await setTimeout(ms('1s'));
  // check for user on our side
  let user = await Users.findOne({
    [config.userFields.stripeCustomerID]: customer.id
  })
    .lean()
    .exec();
  if (!user) return;
  if (user.is_banned) return;

// if emails did not match if (user.email !== customer.email) { logger.info( User email ${user.email} did not match customer email ${customer.email} (${customer.id}) ); customer = await stripe.customers.update(customer.id, { email: user.email }); logger.info(Updated user email to match ${user.email}); }

// check for active subscriptions const [activeSubscriptions, trialingSubscriptions] = await Promise.all([ stripe.subscriptions.list({ customer: customer.id, status: 'active' }), stripe.subscriptions.list({ customer: customer.id, status: 'trialing' }) ]);

// Combine active and trialing subscriptions let subscriptions = [ ...activeSubscriptions.data, ...trialingSubscriptions.data ];

// Handle edge case: multiple subscriptions for one user if (subscriptions.length > 1) { await logger.error( new Error( We may need to refund: User had multiple subscriptions ${user.email} (${customer.id}) ) ); await emailHelper({ template: 'alert', message: { to: config.email.message.from, subject: User had multiple subscriptions ${user.email} }, locals: { message: User ${user.email} (${customer.id}) had multiple subscriptions: ${JSON.stringify( subscriptions.map((s) => s.id) )} } }); } }

Det här jobbet söker efter avvikelser mellan vår databas och Stripe, såsom felaktiga e-postadresser eller flera aktiva prenumerationer. Om den hittar några problem loggar den dem och skickar varningar till vårt adminteam.

PayPal-prenumerationssynkronisering

Vi har ett liknande jobb för PayPal-prenumerationer:

async function syncPayPalSubscriptionPayments() {
  const paypalCustomers = await Users.find({
    $or: [
      {
        [config.userFields.paypalSubscriptionID]: { $exists: true, $ne: null }
      },
      {
        [config.userFields.paypalPayerID]: { $exists: true, $ne: null }
      }
    ]
  })
    // sort by newest customers first
    .sort('-created_at')
    .lean()
    .exec();

await logger.info( Syncing payments for ${paypalCustomers.length} paypal customers );

// Process each customer and sync their payments const errorEmails = await pReduce( paypalCustomers, // Implementation details... ); }

Dessa automatiserade jobb fungerar som vårt sista skyddsnät och säkerställer att vår databas alltid återspeglar det verkliga tillståndet för prenumerationer och betalningar i både Stripe och PayPal.

Hantering av kantfodral

Ett robust betalningssystem måste hantera kantärenden graciöst. Låt oss titta på hur vi hanterar några vanliga scenarier.

Bedrägeriupptäckt och förebyggande

Vi har implementerat sofistikerade bedrägeriupptäckningsmekanismer som automatiskt identifierar och hanterar misstänkta betalningsaktiviteter:

case 'charge.failed': {
  // Get all failed charges in the last 30 days
  const charges = await stripe.charges.list({
    customer: event.data.object.customer,
    created: {
      gte: dayjs().subtract(1, 'month').unix()
    }
  });

// Filter for declined charges const filtered = charges.data.filter( (d) => d.status === 'failed' && d.failure_code === 'card_declined' );

// if not more than 5 then return early if (filtered.length < 5) break;

// Check if user has verified domains const count = await Domains.countDocuments({ members: { $elemMatch: { user: user._id, group: 'admin' } }, plan: { $in: ['enhanced_protection', 'team'] }, has_txt_record: true });

if (!user.is_banned) { // If no verified domains, ban the user and refund all charges if (count === 0) { // Ban the user user.is_banned = true; await user.save();

  // Refund all successful charges
}

} }

Den här koden förbjuder automatiskt användare som har flera misslyckade debiteringar och inga verifierade domäner, vilket är en stark indikator på bedräglig aktivitet.

Tvisthantering

När en användare bestrider en debitering accepterar vi automatiskt anspråket och vidtar lämpliga åtgärder:

case 'CUSTOMER.DISPUTE.CREATED': {
  // accept claim
  const agent = await paypalAgent();
  await agent
    .post(`/v1/customer/disputes/${body.resource.dispute_id}/accept-claim`)
    .send({
      note: 'Full refund to the customer.'
    });

// Find the payment in our database const payment = await Payments.findOne({ $or }); if (!payment) throw new Error('Payment does not exist');

const user = await Users.findById(payment.user); if (!user) throw new Error('User did not exist for customer');

// Cancel the user's subscription if they have one if (isSANB(user[config.userFields.paypalSubscriptionID])) { try { const agent = await paypalAgent(); await agent.post( /v1/billing/subscriptions/${ user[config.userFields.paypalSubscriptionID] }/cancel ); } catch (err) { // Handle subscription cancellation errors } } }

Detta tillvägagångssätt minimerar inverkan av tvister på vår verksamhet samtidigt som det säkerställer en bra kundupplevelse.

Kodåteranvändning: KISS och DRY Principer

Genom hela vårt betalningssystem har vi följt principerna KISS (Keep It Simple, Stupid) och DRY (Don't Repeat Yourself). Här är några exempel:

  1. Delade hjälpfunktioner: Vi har skapat återanvändbara hjälpfunktioner för vanliga uppgifter som att synkronisera betalningar och skicka e-post.

  2. Konsekvent felhantering: Både Stripe- och PayPal-webhookhanterare använder samma mönster för felhantering och administratörsmeddelanden.

  3. Unified Database Schema: Vårt databasschema är utformat för att rymma både Stripe- och PayPal-data, med vanliga fält för betalningsstatus, belopp och planinformation.

  4. Centraliserad konfiguration: Betalningsrelaterad konfiguration är centraliserad i en enda fil, vilket gör det enkelt att uppdatera prissättning och produktinformation.

graph TD
    subgraph "Code Reuse Patterns"
        A[Helper Functions] --> B[syncStripePaymentIntent]
        A --> C[syncPayPalOrderPaymentByPaymentId]
        A --> D[syncPayPalSubscriptionPaymentsByUser]
    end
classDef primary fill:blue,stroke:#333,stroke-width:2px;
classDef secondary fill:red,stroke:#333,stroke-width:1px;

class A,P,V primary;
class B,C,D,E,I,L,Q,R,S,W,X,Y,Z secondary;

graph TD
    subgraph "Code Reuse Patterns"
        E[Error Handling] --> F[Common Error Logging]
        E --> G[Admin Email Notifications]
        E --> H[User Notifications]
    end

    classDef primary fill:blue,stroke:#333,stroke-width:2px;
    classDef secondary fill:red,stroke:#333,stroke-width:1px;

    class A,P,V primary;
    class B,C,D,E,I,L,Q,R,S,W,X,Y,Z secondary;
graph TD
    subgraph "Code Reuse Patterns"
        I[Configuration] --> J[Centralized Payment Config]
        I --> K[Shared Environment Variables]
    end

    classDef primary fill:blue,stroke:#333,stroke-width:2px;
    classDef secondary fill:red,stroke:#333,stroke-width:1px;

    class A,P,V primary;
    class B,C,D,E,I,L,Q,R,S,W,X,Y,Z secondary;
graph TD
    subgraph "Code Reuse Patterns"
        L[Webhook Processing] --> M[Signature Verification]
        L --> N[Async Event Processing]
        L --> O[Background Processing]
    end

    classDef primary fill:blue,stroke:#333,stroke-width:2px;
    classDef secondary fill:red,stroke:#333,stroke-width:1px;

    class A,P,V primary;
    class B,C,D,E,I,L,Q,R,S,W,X,Y,Z secondary;
graph TD
    subgraph "KISS Principle"
        P[Simple Data Flow] --> Q[Unidirectional Updates]
        P --> R[Clear Responsibility Separation]

        S[Explicit Error Handling] --> T[No Silent Failures]
        S --> U[Comprehensive Logging]
    end

    classDef primary fill:blue,stroke:#333,stroke-width:2px;
    classDef secondary fill:red,stroke:#333,stroke-width:1px;

    class A,P,V primary;
    class B,C,D,E,I,L,Q,R,S,W,X,Y,Z secondary;
graph TD
    subgraph "DRY Principle"
        V[Shared Logic] --> W[Payment Processing Functions]
        V --> X[Email Templates]
        V --> Y[Validation Logic]

        Z[Common Database Operations] --> AA[User Updates]
        Z --> AB[Payment Recording]
    end

    classDef primary fill:blue,stroke:#333,stroke-width:2px;
    classDef secondary fill:red,stroke:#333,stroke-width:1px;

    class A,P,V primary;
    class B,C,D,E,I,L,Q,R,S,W,X,Y,Z secondary;

Implementering av VISA-prenumerationskrav

Utöver vår trifecta-metod har vi implementerat specifika funktioner för att uppfylla VISAs prenumerationskrav samtidigt som vi förbättrar användarupplevelsen. Ett nyckelkrav från VISA är att användare måste meddelas innan de debiteras för ett abonnemang, särskilt när de går över från en provperiod till en betalprenumeration.

Automatiserade e-postmeddelanden före förnyelse

Vi har byggt ett automatiskt system som identifierar användare med aktiva provprenumerationer och skickar dem ett meddelande via e-post innan deras första debitering sker. Detta håller oss inte bara kompatibla med VISA-kraven utan minskar också återkrav och förbättrar kundnöjdheten.

Så här implementerade vi den här funktionen:

// Find users with trial subscriptions who haven't received a notification yet
const users = await Users.find({
  $or: [
    {
      $and: [
        { [config.userFields.stripeSubscriptionID]: { $exists: true } },
        { [config.userFields.stripeTrialSentAt]: { $exists: false } },
        // Exclude subscriptions that have already had payments
        ...(paidStripeSubscriptionIds.length > 0
          ? [
              {
                [config.userFields.stripeSubscriptionID]: {
                  $nin: paidStripeSubscriptionIds
                }
              }
            ]
          : [])
      ]
    },
    {
      $and: [
        { [config.userFields.paypalSubscriptionID]: { $exists: true } },
        { [config.userFields.paypalTrialSentAt]: { $exists: false } },
        // Exclude subscriptions that have already had payments
        ...(paidPayPalSubscriptionIds.length > 0
          ? [
              {
                [config.userFields.paypalSubscriptionID]: {
                  $nin: paidPayPalSubscriptionIds
                }
              }
            ]
          : [])
      ]
    }
  ]
});

// Process each user and send notification for (const user of users) { // Get subscription details from payment processor const subscription = await getSubscriptionDetails(user);

// Calculate subscription duration and frequency const duration = getDurationFromPlanId(subscription.plan_id); const frequency = getHumanReadableFrequency(duration, user.locale); const amount = getPlanAmount(user.plan, duration);

// Get user's domains for personalized email const domains = await Domains.find({ 'members.user': user._id }).sort('name').lean().exec();

// Send VISA-compliant notification email await emailHelper({ template: 'visa-trial-subscription-requirement', message: { to: user.receipt_email || user.email, ...(user.receipt_email ? { cc: user.email } : {}) }, locals: { user, firstChargeDate: new Date(subscription.start_time), frequency, formattedAmount: numeral(amount).format('$0,0,0.00'), domains } });

// Record that notification was sent await Users.findByIdAndUpdate(user._id, { $set: { [config.userFields.paypalTrialSentAt]: new Date() } }); }

Denna implementering säkerställer att användarna alltid är informerade om kommande avgifter, med tydliga detaljer om:

  1. När den första laddningen kommer att ske
  2. Frekvensen av framtida avgifter (månatlig, årlig, etc.)
  3. Det exakta beloppet de kommer att debiteras
  4. Vilka domäner som omfattas av deras prenumeration

Genom att automatisera denna process upprätthåller vi perfekt överensstämmelse med VISA:s krav (som kräver avisering minst 7 dagar före laddning) samtidigt som vi minskar supportförfrågningar och förbättrar den övergripande användarupplevelsen.

Hantering av kantfodral

Vår implementering inkluderar även robust felhantering. Om något går fel under aviseringsprocessen varnar vårt system automatiskt vårt team:

try {
  await mapper(user);
} catch (err) {
  logger.error(err);

// Send alert to administrators await emailHelper({ template: 'alert', message: { to: config.email.message.from, subject: 'VISA Trial Subscription Requirement Error' }, locals: { message: &#x3C;pre>&#x3C;code>${safeStringify( parseErr(err), null, 2 )}&#x3C;/code>&#x3C;/pre> } }); }

Detta säkerställer att även om det finns ett problem med aviseringssystemet, kan vårt team snabbt åtgärda det och upprätthålla överensstämmelse med VISAs krav.

VISA-prenumerationsaviseringssystemet är ytterligare ett exempel på hur vi har byggt vår betalningsinfrastruktur med både efterlevnad och användarupplevelse i åtanke, och kompletterar vår trifecta-metod för att säkerställa pålitlig, transparent betalningshantering.

Provperioder och prenumerationsvillkor

För användare som aktiverar automatisk förnyelse av befintliga planer, beräknar vi lämplig provperiod för att säkerställa att de inte debiteras förrän deras nuvarande plan löper ut:

if (
  isEnableAutoRenew &&
  dayjs(ctx.state.user[config.userFields.planExpiresAt]).isAfter(
    dayjs()
  )
) {
  const hours = dayjs(
    ctx.state.user[config.userFields.planExpiresAt]
  ).diff(dayjs(), 'hours');

// Handle trial period calculation }

Vi tillhandahåller också tydlig information om prenumerationsvillkor, inklusive faktureringsfrekvens och avbokningspolicyer, och inkluderar detaljerad metadata med varje prenumeration för att säkerställa korrekt spårning och hantering.

Slutsats: Fördelarna med vår Trifecta-metod

Vår trifecta-inställning till betalningshantering har gett flera viktiga fördelar:

  1. Pålitlighet: Genom att implementera tre lager av betalningsverifiering säkerställer vi att ingen betalning missas eller behandlas felaktigt.

  2. Noggrannhet: Vår databas återspeglar alltid det verkliga tillståndet för prenumerationer och betalningar i både Stripe och PayPal.

  3. Flexibilitet: Användare kan välja sin föredragna betalningsmetod utan att kompromissa med tillförlitligheten hos vårt system.

  4. Robusthet: Vårt system hanterar edge-ärenden elegant, från nätverksfel till bedrägliga aktiviteter.

Om du implementerar ett betalningssystem som stöder flera processorer rekommenderar vi starkt denna trifecta-metod. Det kräver mer utvecklingsinsatser i förväg, men de långsiktiga fördelarna när det gäller tillförlitlighet och noggrannhet är väl värda det.

För mer information om Forward Email och våra integritetsfokuserade e-posttjänster, besök vår webbplats.