Trifecta-tilnærmingen: Hvordan videresende e-post bygget et skuddsikkert betalingssystem med Stripe og PayPal

Lær hvordan utviklingsteamet vårt integrerte både Stripe og PayPal ved å bruke en trifecta-tilnærming som sikrer 1:1 sanntidsnøyaktighet på tvers av hele systemet vårt.

Forord

Hos Forward Email har vi alltid prioritert å lage systemer som er pålitelige, nøyaktige og brukervennlige. Når det kom til å implementere betalingsbehandlingssystemet vårt, visste vi at vi trengte en løsning som kunne håndtere flere betalingsbehandlere og samtidig opprettholde perfekt datakonsistens. Dette blogginnlegget beskriver hvordan utviklingsteamet vårt integrerte både Stripe og PayPal ved å bruke en trifecta-tilnærming som sikrer 1:1 sanntidsnøyaktighet på tvers av hele systemet vårt.

Utfordringen: Flere betalingsbehandlere, én kilde til sannhet

Som en personvernfokusert e-posttjeneste ønsket vi å gi brukerne betalingsalternativer. Noen foretrekker enkelheten med kredittkortbetalinger gjennom Stripe, mens andre verdsetter det ekstra laget med separasjon som PayPal gir. Støtte for flere betalingsbehandlere introduserer imidlertid betydelig kompleksitet:

  1. Hvordan sikrer vi konsistente data på tvers av ulike betalingssystemer?
  2. Hvordan håndterer vi edge-saker som tvister, refusjoner eller mislykkede betalinger?
  3. Hvordan opprettholder vi en enkelt kilde til sannhet i databasen vår?

Vår løsning var å implementere det vi kaller «trifecta-tilnærmingen» – et tre-lags system som gir redundans og sikrer datakonsistens uansett hva som skjer.

Trifecta-tilnærmingen: Tre lag av pålitelighet

Betalingssystemet vårt består av tre kritiske komponenter som jobber sammen for å sikre perfekt datasynkronisering:

  1. Omdirigeringer etter utsjekking - Registrere betalingsinformasjon umiddelbart etter kassen
  2. Webhook-behandlere - Behandling av sanntidshendelser fra betalingsbehandlere
  3. Automatiserte jobber - Periodisk verifisering og avstemming av betalingsdata

La oss dykke ned i hver komponent og se hvordan de fungerer sammen.

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;

Lag 1: Omdirigeringer etter utsjekking

Det første laget av vår trifecta-tilnærming skjer umiddelbart etter at en bruker har fullført en betaling. Både Stripe og PayPal gir mekanismer for å omdirigere brukere tilbake til nettstedet vårt med transaksjonsinformasjon.

Stripe Checkout Implementering

For Stripe bruker vi deres Checkout Sessions API for å skape en sømløs betalingsopplevelse. Når en bruker velger en plan og velger å betale med et kredittkort, oppretter vi en utsjekkingsøkt med spesifikk suksess og kansellerer nettadresser:

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 kritiske delen her er success_url parameter, som inkluderer session_id som en spørringsparameter. Når Stripe omdirigerer brukeren tilbake til nettstedet vårt etter en vellykket betaling, kan vi bruke denne økt-IDen til å bekrefte transaksjonen og oppdatere databasen vår deretter.

PayPal-betalingsflyt

For PayPal bruker vi en lignende tilnærming med deres ordre-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, spesifiserer vi return_url og cancel_url parametere for å håndtere omdirigeringer etter betaling. Når PayPal omdirigerer brukeren tilbake til nettstedet vårt, kan vi fange opp betalingsdetaljene og oppdatere databasen vår.

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

Lag 2: Webhook-håndtere med signaturverifisering

Selv om omdirigeringer etter utsjekking fungerer bra for de fleste scenarier, er de ikke idiotsikre. Brukere kan lukke nettleseren før de blir omdirigert, eller nettverksproblemer kan forhindre at omdirigeringen fullføres. Det er her webhooks kommer inn.

Både Stripe og PayPal tilbyr webhook-systemer som sender sanntidsvarsler om betalingshendelser. Vi har implementert robuste webhook-behandlere som bekrefter ektheten til disse varslene og behandler dem deretter.

Stripe Webhook Implementering

Vår Stripe webhook-behandler verifiserer signaturen til innkommende webhook-hendelser for å sikre at de er legitime:

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 funksjonen verifiserer signaturen ved å bruke endepunktshemmeligheten vår. Hvis signaturen er gyldig, behandler vi hendelsen asynkront for å unngå å blokkere webhook-svaret.

PayPal Webhook-implementering

På samme måte verifiserer vår PayPal webhook-behandler ektheten av innkommende varsler:

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));
    });
}

Begge webhook-behandlerne følger samme mønster: verifiser signaturen, bekrefter mottak og behandle hendelsen asynkront. Dette sikrer at vi aldri går glipp av en betalingshendelse, selv om omdirigeringen etter utsjekking mislykkes.

Lag 3: Automatiserte jobber med Bree

Det siste laget av vår trifecta-tilnærming er et sett med automatiserte jobber som periodisk verifiserer og avstemmer betalingsdata. Vi bruker Bree, en jobbplanlegger for Node.js, for å kjøre disse jobbene med jevne mellomrom.

Abonnementsnøyaktighetskontroll

En av nøkkeljobbene våre er sjekkeren for nøyaktighet av abonnementer, som sikrer at databasen vår nøyaktig gjenspeiler abonnementsstatusen 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) )} } }); } }

Denne jobben sjekker for avvik mellom databasen vår og Stripe, for eksempel feilaktige e-postadresser eller flere aktive abonnementer. Hvis den finner noen problemer, logger den dem og sender varsler til administrasjonsteamet vårt.

PayPal-abonnementssynkronisering

Vi har en lignende jobb for PayPal-abonnementer:

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... ); }

Disse automatiserte jobbene fungerer som vårt siste sikkerhetsnett, og sikrer at databasen vår alltid gjenspeiler den sanne tilstanden til abonnementer og betalinger i både Stripe og PayPal.

Håndtering av kantsaker

Et robust betalingssystem må håndtere kantsaker elegant. La oss se på hvordan vi håndterer noen vanlige scenarier.

Oppdagelse og forebygging av svindel

Vi har implementert sofistikerte svindeldeteksjonsmekanismer som automatisk identifiserer og håndterer mistenkelige betalingsaktiviteter:

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
}

} }

Denne koden utestenger automatisk brukere som har flere mislykkede belastninger og ingen verifiserte domener, noe som er en sterk indikator på uredelig aktivitet.

Tvistehåndtering

Når en bruker bestrider en belastning, godtar vi automatisk kravet og iverksetter passende tiltak:

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 } } }

Denne tilnærmingen minimerer virkningen av tvister på virksomheten vår samtidig som den sikrer en god kundeopplevelse.

Kode Gjenbruk: KISS og DRY-prinsipper

Gjennom hele betalingssystemet vårt har vi fulgt prinsippene KISS (Keep It Simple, Stupid) og DRY (Ikke gjenta deg selv). Her er noen eksempler:

  1. Delte hjelpefunksjoner: Vi har laget gjenbrukbare hjelpefunksjoner for vanlige oppgaver som synkronisering av betalinger og sending av e-post.

  2. Konsekvent feilhåndtering: Både Stripe- og PayPal-webhook-behandlere bruker det samme mønsteret for feilhåndtering og admin-varsler.

  3. Unified Database Schema: Vårt databaseskjema er designet for å imøtekomme både Stripe- og PayPal-data, med vanlige felt for betalingsstatus, beløp og planinformasjon.

  4. Sentralisert konfigurasjon: Betalingsrelatert konfigurasjon er sentralisert i én enkelt fil, noe som gjør det enkelt å oppdatere priser og produktinformasjon.

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-abonnementskrav

I tillegg til vår trifecta-tilnærming, har vi implementert spesifikke funksjoner for å overholde VISAs abonnementskrav og samtidig forbedre brukeropplevelsen. Et sentralt krav fra VISA er at brukere må varsles før de belastes for et abonnement, spesielt ved overgang fra prøveversjon til betalt abonnement.

Automatiserte e-postvarsler før fornyelse

Vi har bygget et automatisert system som identifiserer brukere med aktive prøveabonnementer og sender dem en e-postvarsling før deres første belastning skjer. Dette holder oss ikke bare i samsvar med VISA-kravene, men reduserer også tilbakeføringer og forbedrer kundetilfredsheten.

Slik implementerte vi denne funksjonen:

// 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() } }); }

Denne implementeringen sikrer at brukerne alltid er informert om kommende gebyrer, med klare detaljer om:

  1. Når den første ladingen vil skje
  2. Hyppigheten av fremtidige avgifter (månedlig, årlig, etc.)
  3. Det nøyaktige beløpet de vil bli belastet
  4. Hvilke domener dekkes av deres abonnement

Ved å automatisere denne prosessen opprettholder vi perfekt overholdelse av VISAs krav (som krever varsling minst 7 dager før lading) samtidig som vi reduserer støtteforespørsler og forbedrer den generelle brukeropplevelsen.

Håndtering av kantsaker

Vår implementering inkluderer også robust feilhåndtering. Hvis noe går galt under varslingsprosessen, varsler systemet vårt automatisk:

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> } }); }

Dette sikrer at selv om det er et problem med varslingssystemet, kan teamet vårt raskt løse det og opprettholde samsvar med VISAs krav.

VISA-abonnementsvarslingssystemet er et annet eksempel på hvordan vi har bygget betalingsinfrastrukturen vår med tanke på både overholdelse og brukeropplevelse, og komplementerer vår trifecta-tilnærming for å sikre pålitelig, gjennomsiktig betalingsbehandling.

Prøveperioder og abonnementsvilkår

For brukere som aktiverer automatisk fornyelse på eksisterende planer, beregner vi riktig prøveperiode for å sikre at de ikke belastes før deres nåværende plan utløper:

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 gir også tydelig informasjon om abonnementsvilkår, inkludert faktureringsfrekvens og kanselleringspolicyer, og inkluderer detaljerte metadata med hvert abonnement for å sikre riktig sporing og administrasjon.

Konklusjon: Fordelene med vår Trifecta-tilnærming

Vår trifecta-tilnærming til betalingsbehandling har gitt flere viktige fordeler:

  1. Pålitelighet: Ved å implementere tre lag med betalingsverifisering sikrer vi at ingen betaling går glipp av eller blir feilbehandlet.

  2. Nøyaktighet: Vår database gjenspeiler alltid den sanne tilstanden til abonnementer og betalinger i både Stripe og PayPal.

  3. Fleksibilitet: Brukere kan velge sin foretrukne betalingsmetode uten å kompromittere påliteligheten til systemet vårt.

  4. Robusthet: Systemet vårt håndterer edge-saker elegant, fra nettverksfeil til uredelige aktiviteter.

Hvis du implementerer et betalingssystem som støtter flere prosessorer, anbefaler vi denne trifecta-tilnærmingen på det sterkeste. Det krever mer utviklingsinnsats på forhånd, men de langsiktige fordelene når det gjelder pålitelighet og nøyaktighet er vel verdt det.

For mer informasjon om Videresend e-post og våre personvernfokuserte e-posttjenester, besøk vår nettsted.