Stripe ve PayPal ile Güçlü Bir Ödeme Sistemi Nasıl Kurduk: Üçlü Yaklaşım
Önsöz
Forward Email olarak, her zaman güvenilir, doğru ve kullanıcı dostu sistemler oluşturmayı önceliklendirdik. Ödeme işleme sistemimizi uygularken, birden fazla ödeme işleyicisini yönetebilen ve mükemmel veri tutarlılığı sağlayan bir çözüme ihtiyacımız olduğunu biliyorduk. Bu blog yazısı, geliştirme ekibimizin Stripe ve PayPal’ı nasıl entegre ettiğini ve tüm sistemimizde 1:1 gerçek zamanlı doğruluk sağlayan üçlü yaklaşımı detaylandırmaktadır.
Zorluk: Birden Fazla Ödeme İşleyicisi, Tek Doğru Kaynak
Gizliliğe odaklı bir e-posta servisi olarak, kullanıcılarımıza ödeme seçenekleri sunmak istedik. Bazıları Stripe üzerinden kredi kartı ödemelerinin sadeliğini tercih ederken, diğerleri PayPal’ın sağladığı ek ayrım katmanını değerli buluyor. Ancak, birden fazla ödeme işleyicisini desteklemek önemli karmaşıklıklar getiriyor:
- Farklı ödeme sistemlerinde tutarlı veriyi nasıl sağlarız?
- İhtilaflar, iadeler veya başarısız ödemeler gibi kenar durumları nasıl yönetiriz?
- Veritabanımızda tek bir doğru kaynak nasıl korunur?
Çözümümüz, “üçlü yaklaşım” dediğimiz - yedeklilik sağlayan ve ne olursa olsun veri tutarlılığını garanti eden üç katmanlı bir sistem uygulamaktı.
Üçlü Yaklaşım: Üç Katmanlı Güvenilirlik
Ödeme sistemimiz, mükemmel veri senkronizasyonunu sağlamak için birlikte çalışan üç kritik bileşenden oluşur:
- Ödeme sonrası yönlendirmeler - Ödeme bilgilerini hemen ödeme sonrası yakalamak
- Webhook işleyicileri - Ödeme işleyicilerinden gerçek zamanlı olayları işlemek
- Otomatik işler - Ödeme verilerini periyodik olarak doğrulamak ve uzlaştırmak
Her bileşene yakından bakalım ve nasıl birlikte çalıştıklarını görelim.
Katman 1: Ödeme Sonrası Yönlendirmeler
Üçlü yaklaşımımızın ilk katmanı, bir kullanıcı ödemeyi tamamlar tamamlamaz gerçekleşir. Hem Stripe hem de PayPal, kullanıcıları işlem bilgileriyle birlikte sitemize geri yönlendirmek için mekanizmalar sağlar.
Stripe Checkout Uygulaması
Stripe için, sorunsuz bir ödeme deneyimi yaratmak amacıyla Checkout Sessions API'sini kullanıyoruz. Bir kullanıcı bir plan seçip kredi kartıyla ödemeyi tercih ettiğinde, belirli başarı ve iptal URL'leri ile bir Checkout Session oluşturuyoruz:
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
};
// Checkout oturumunu oluştur ve yönlendir
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 };
}
Buradaki kritik kısım, success_url parametresidir; bu parametre, sorgu parametresi olarak session_id içerir. Stripe, başarılı bir ödeme sonrası kullanıcıyı sitemize geri yönlendirdiğinde, bu oturum ID'sini kullanarak işlemi doğrulayabilir ve veritabanımızı buna göre güncelleyebiliriz.
PayPal Ödeme Akışı
PayPal için, benzer bir yaklaşımı Orders API ile kullanıyoruz:
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'
}
]
}
]
};
Stripe'da olduğu gibi, ödeme sonrası yönlendirmeleri yönetmek için return_url ve cancel_url parametrelerini belirtiyoruz. PayPal kullanıcıyı sitemize geri yönlendirdiğinde, ödeme detaylarını yakalayıp veritabanımızı güncelleyebiliriz.
Katman 2: İmza Doğrulamalı Webhook İşleyicileri
Post-checkout yönlendirmeleri çoğu senaryo için iyi çalışsa da, kusursuz değildir. Kullanıcılar yönlendirilmeden önce tarayıcılarını kapatabilir veya ağ sorunları yönlendirmenin tamamlanmasını engelleyebilir. İşte burada webhooklar devreye girer.
Hem Stripe hem de PayPal, ödeme olayları hakkında gerçek zamanlı bildirimler gönderen webhook sistemleri sağlar. Bu bildirimlerin doğruluğunu doğrulayan ve bunları uygun şekilde işleyen sağlam webhook işleyicileri uyguladık.
Stripe Webhook Uygulaması
Stripe webhook işleyicimiz, gelen webhook olaylarının imzasını doğrulayarak bunların meşru olduğunu garanti eder:
async function webhook(ctx) {
const sig = ctx.request.get('stripe-signature');
// bir sorun varsa hata fırlat
if (!isSANB(sig))
throw Boom.badRequest(ctx.translateError('INVALID_STRIPE_SIGNATURE'));
const event = stripe.webhooks.constructEvent(
ctx.request.rawBody,
sig,
env.STRIPE_ENDPOINT_SECRET
);
// bir sorun varsa hata fırlat
if (!event)
throw Boom.badRequest(ctx.translateError('INVALID_STRIPE_SIGNATURE'));
ctx.logger.info('stripe webhook', { event });
// olayı aldığımızı onaylamak için yanıt döndür
ctx.body = { received: true };
// arka planda çalıştır
processEvent(ctx, event)
.then()
.catch((err) => {
ctx.logger.fatal(err, { event });
// yöneticiye hata e-postası gönder
emailHelper({
template: 'alert',
message: {
to: config.email.message.from,
subject: `Stripe Webhook Hatası (Olay ID ${event.id})`
},
locals: {
message: `<pre><code>${safeStringify(
parseErr(err),
null,
2
)}</code></pre>`
}
})
.then()
.catch((err) => ctx.logger.fatal(err, { event }));
});
}
stripe.webhooks.constructEvent fonksiyonu, uç nokta sırrımızı kullanarak imzayı doğrular. İmza geçerliyse, webhook yanıtını engellememek için olayı asenkron olarak işleriz.
PayPal Webhook Uygulaması
Benzer şekilde, PayPal webhook işleyicimiz gelen bildirimlerin doğruluğunu doğrular:
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);
// bir sorun varsa hata fırlat
if (!_.isObject(response) || response.verification_status !== 'SUCCESS')
throw Boom.badRequest(ctx.translateError('INVALID_PAYPAL_SIGNATURE'));
// olayı aldığımızı onaylamak için yanıt döndür
ctx.body = { received: true };
// arka planda çalıştır
processEvent(ctx)
.then()
.catch((err) => {
ctx.logger.fatal(err);
// yöneticiye hata e-postası gönder
emailHelper({
template: 'alert',
message: {
to: config.email.message.from,
subject: `PayPal Webhook Hatası (Olay ID ${ctx.request.body.id})`
},
locals: {
message: `<pre><code>${safeStringify(
parseErr(err),
null,
2
)}</code></pre>`
}
})
.then()
.catch((err) => ctx.logger.fatal(err));
});
}
Her iki webhook işleyicisi de aynı deseni takip eder: imzayı doğrula, alındığını onayla ve olayı asenkron olarak işle. Bu, post-checkout yönlendirmesi başarısız olsa bile hiçbir ödeme olayını kaçırmamamızı sağlar.
Katman 3: Bree ile Otomatik İşler
Üçlü yaklaşımımızın son katmanı, ödeme verilerini periyodik olarak doğrulayan ve mutabakat yapan otomatik işlerden oluşur. Bu işleri düzenli aralıklarla çalıştırmak için Node.js için bir iş zamanlayıcısı olan Bree'yi kullanıyoruz.
Abonelik Doğruluk Denetleyicisi
Ana işlerimizden biri olan abonelik doğruluk denetleyicisi, veritabanımızın Stripe'daki abonelik durumunu doğru şekilde yansıttığından emin olur:
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)
)}`
}
});
}
}
This job checks for discrepancies between our database and Stripe, such as mismatched email addresses or multiple active subscriptions. If it finds any issues, it logs them and sends alerts to our admin team.
PayPal Subscription Synchronization
We have a similar job for PayPal subscriptions:
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...
);
}
These automated jobs serve as our final safety net, ensuring that our database always reflects the true state of subscriptions and payments in both Stripe and PayPal.
Handling Edge Cases
A robust payment system must handle edge cases gracefully. Let's look at how we handle some common scenarios.
Fraud Detection and Prevention
We've implemented sophisticated fraud detection mechanisms that automatically identify and handle suspicious payment activities:
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
}
}
}
Bu kod, birden fazla başarısız ödeme girişimi olan ve doğrulanmış alan adı bulunmayan kullanıcıları otomatik olarak engeller; bu, dolandırıcılık faaliyetlerinin güçlü bir göstergesidir.
İtiraz İşleme
Bir kullanıcı bir ödemeye itiraz ettiğinde, talebi otomatik olarak kabul eder ve uygun işlemi yaparız:
case 'CUSTOMER.DISPUTE.CREATED': {
// talebi kabul et
const agent = await paypalAgent();
await agent
.post(`/v1/customer/disputes/${body.resource.dispute_id}/accept-claim`)
.send({
note: 'Müşteriye tam geri ödeme.'
});
// Ödemeyi veritabanımızda bul
const payment = await Payments.findOne({ $or });
if (!payment) throw new Error('Ödeme mevcut değil');
const user = await Users.findById(payment.user);
if (!user) throw new Error('Müşteri için kullanıcı mevcut değildi');
// Kullanıcının aboneliği varsa iptal et
if (isSANB(user[config.userFields.paypalSubscriptionID])) {
try {
const agent = await paypalAgent();
await agent.post(
`/v1/billing/subscriptions/${
user[config.userFields.paypalSubscriptionID]
}/cancel`
);
} catch (err) {
// Abonelik iptali hatalarını yönet
}
}
}
Bu yaklaşım, itirazların işimize etkisini en aza indirirken iyi bir müşteri deneyimi sağlar.
Kod Tekrarı: KISS ve DRY İlkeleri
Ödeme sistemimiz boyunca, KISS (Keep It Simple, Stupid - Basit Tut, Aptal) ve DRY (Don't Repeat Yourself - Kendini Tekrarlama) ilkelerine bağlı kaldık. İşte bazı örnekler:
-
Paylaşılan Yardımcı Fonksiyonlar: Ödemeleri senkronize etmek ve e-posta göndermek gibi yaygın görevler için yeniden kullanılabilir yardımcı fonksiyonlar oluşturduk.
-
Tutarlı Hata Yönetimi: Hem Stripe hem de PayPal webhook işleyicileri, hata yönetimi ve yönetici bildirimleri için aynı deseni kullanır.
-
Birleşik Veritabanı Şeması: Veritabanı şemamız, ödeme durumu, tutar ve plan bilgisi gibi ortak alanlarla hem Stripe hem de PayPal verilerini barındıracak şekilde tasarlanmıştır.
-
Merkezi Konfigürasyon: Ödeme ile ilgili konfigürasyon tek bir dosyada toplanmıştır, böylece fiyatlandırma ve ürün bilgilerini güncellemek kolaydır.
graph TD subgraph "DRY Prensibi" V[Paylaşılan Mantık] --> W[Ödeme İşleme Fonksiyonları] V --> X[E-posta Şablonları] V --> Y[Doğrulama Mantığı]
Z[Ortak Veritabanı İşlemleri] --> AA[Kullanıcı Güncellemeleri]
Z --> AB[Ödeme Kaydı]
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;
## VISA Abonelik Gereksinimleri Uygulaması {#visa-subscription-requirements-implementation}
Üçlü yaklaşımımıza ek olarak, VISA'nın abonelik gereksinimlerine uyum sağlamak ve kullanıcı deneyimini geliştirmek için belirli özellikler uyguladık. VISA'nın önemli bir gereksinimi, kullanıcıların abonelik için ücretlendirilmeden önce, özellikle deneme sürümünden ücretli aboneliğe geçerken, bilgilendirilmesidir.
### Otomatik Ön-Yenileme E-posta Bildirimleri {#automated-pre-renewal-email-notifications}
Aktif deneme aboneliği olan kullanıcıları tespit eden ve ilk ücretlendirme gerçekleşmeden önce onlara bildirim e-postası gönderen otomatik bir sistem kurduk. Bu, sadece VISA gereksinimlerine uyum sağlamamıza yardımcı olmakla kalmaz, aynı zamanda geri ödeme taleplerini azaltır ve müşteri memnuniyetini artırır.
Bu özelliği şöyle uyguladık:
```javascript
// Bildirim almamış deneme aboneliği olan kullanıcıları bul
const users = await Users.find({
$or: [
{
$and: [
{ [config.userFields.stripeSubscriptionID]: { $exists: true } },
{ [config.userFields.stripeTrialSentAt]: { $exists: false } },
// Zaten ödeme yapılmış abonelikleri hariç tut
...(paidStripeSubscriptionIds.length > 0
? [
{
[config.userFields.stripeSubscriptionID]: {
$nin: paidStripeSubscriptionIds
}
}
]
: [])
]
},
{
$and: [
{ [config.userFields.paypalSubscriptionID]: { $exists: true } },
{ [config.userFields.paypalTrialSentAt]: { $exists: false } },
// Zaten ödeme yapılmış abonelikleri hariç tut
...(paidPayPalSubscriptionIds.length > 0
? [
{
[config.userFields.paypalSubscriptionID]: {
$nin: paidPayPalSubscriptionIds
}
}
]
: [])
]
}
]
});
// Her kullanıcı için işlemleri yap ve bildirim gönder
for (const user of users) {
// Ödeme işlemcisinden abonelik detaylarını al
const subscription = await getSubscriptionDetails(user);
// Abonelik süresi ve sıklığını hesapla
const duration = getDurationFromPlanId(subscription.plan_id);
const frequency = getHumanReadableFrequency(duration, user.locale);
const amount = getPlanAmount(user.plan, duration);
// Kişiselleştirilmiş e-posta için kullanıcının alan adlarını al
const domains = await Domains.find({
'members.user': user._id
}).sort('name').lean().exec();
// VISA uyumlu bildirim e-postası gönder
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
}
});
// Bildirimin gönderildiğini kaydet
await Users.findByIdAndUpdate(user._id, {
$set: {
[config.userFields.paypalTrialSentAt]: new Date()
}
});
}
Bu uygulama, kullanıcıların yaklaşan ücretlendirmeler hakkında her zaman bilgilendirilmelerini sağlar ve açıkça şunları içerir:
- İlk ücretlendirme ne zaman gerçekleşecek
- Gelecekteki ücretlendirmelerin sıklığı (aylık, yıllık vb.)
- Ücretlendirilecek kesin tutar
- Abonelik kapsamındaki alan adları
Bu süreci otomatikleştirerek, VISA'nın gerektirdiği (ücretlendirmeden en az 7 gün önce bildirim yapılması zorunluluğu) tam uyumu sağlarken, destek taleplerini azaltır ve genel kullanıcı deneyimini iyileştiririz.
Kenar Durumların Yönetimi
Uygulamamız ayrıca sağlam hata yönetimini de içerir. Bildirim sürecinde herhangi bir sorun oluşursa, sistemimiz otomatik olarak ekibimizi uyarır:
try {
await mapper(user);
} catch (err) {
logger.error(err);
// Yöneticilere uyarı gönder
await emailHelper({
template: 'alert',
message: {
to: config.email.message.from,
subject: 'VISA Deneme Aboneliği Gereksinim Hatası'
},
locals: {
message: `<pre><code>${safeStringify(
parseErr(err),
null,
2
)}</code></pre>`
}
});
}
Bu, bildirim sisteminde bir sorun olsa bile ekibimizin hızlıca müdahale edip VISA'nın gereksinimlerine uyumu sürdürebilmesini sağlar.
VISA abonelik bildirim sistemi, ödeme altyapımızı hem uyumluluk hem de kullanıcı deneyimi göz önünde bulundurarak nasıl inşa ettiğimizin bir diğer örneğidir. Güvenilir, şeffaf ödeme işlemi sağlamak için üçlü yaklaşımımızı tamamlar.
Deneme Süreleri ve Abonelik Şartları
Mevcut planlarda otomatik yenilemeyi etkinleştiren kullanıcılar için, mevcut planları sona erene kadar ücretlendirilmemelerini sağlamak amacıyla uygun deneme süresini hesaplıyoruz:
if (
isEnableAutoRenew &&
dayjs(ctx.state.user[config.userFields.planExpiresAt]).isAfter(
dayjs()
)
) {
const hours = dayjs(
ctx.state.user[config.userFields.planExpiresAt]
).diff(dayjs(), 'hours');
// Deneme süresi hesaplamasını yönet
}
Ayrıca, faturalandırma sıklığı ve iptal politikaları dahil olmak üzere abonelik şartları hakkında net bilgiler sağlıyor ve her abonelikle birlikte doğru takip ve yönetim için detaylı meta veriler ekliyoruz.
Sonuç: Üçlü Yaklaşımımızın Faydaları
Ödeme işlemlerinde kullandığımız üçlü yaklaşım birkaç önemli fayda sağlamıştır:
-
Güvenilirlik: Üç katmanlı ödeme doğrulaması uygulayarak hiçbir ödemenin kaçırılmamasını veya yanlış işlenmemesini sağlıyoruz.
-
Doğruluk: Veritabanımız her zaman Stripe ve PayPal’daki abonelikler ve ödemelerin gerçek durumunu yansıtır.
-
Esneklik: Kullanıcılar tercih ettikleri ödeme yöntemini sistemimizin güvenilirliğinden ödün vermeden seçebilirler.
-
Dayanıklılık: Sistemimiz, ağ hatalarından dolandırıcılık faaliyetlerine kadar kenar durumları sorunsuz yönetir.
Birden fazla ödeme işlemcisini destekleyen bir ödeme sistemi uyguluyorsanız, bu üçlü yaklaşımı şiddetle tavsiye ederiz. Başlangıçta daha fazla geliştirme çabası gerektirir, ancak uzun vadede güvenilirlik ve doğruluk açısından sağladığı faydalar buna değerdir.
Forward Email ve gizlilik odaklı e-posta hizmetlerimiz hakkında daha fazla bilgi için web sitemizi ziyaret edin.