Skip to content

Commit

Permalink
fix: fixed dup payment intent (check for payment status successful), …
Browse files Browse the repository at this point in the history
…fixed billing reminder job, added stack trace to all instances of Payments.create
  • Loading branch information
titanism committed Sep 29, 2023
1 parent 351a1fd commit 83fa56b
Show file tree
Hide file tree
Showing 36 changed files with 155 additions and 60 deletions.
3 changes: 2 additions & 1 deletion app/controllers/api/v1/paypal.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@ async function processEvent(ctx) {
kind: 'one-time',
paypal_order_id: res.body.id,
paypal_transaction_id: transactionId,
invoice_at: now
invoice_at: now,
stack: new Error('stack').stack
});

// log the payment just for sanity
Expand Down
47 changes: 44 additions & 3 deletions app/controllers/web/my-account/retrieve-domain-billing.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ async function retrieveDomainBilling(ctx) {
await Payments.create(
ctx.state.conversion[ctx.query.plan].map((payment) => ({
...payment,
stack: new Error('stack').stack,
invoice_at: now
}))
);
Expand Down Expand Up @@ -511,6 +512,24 @@ async function retrieveDomainBilling(ctx) {

if (!paymentIntent) throw ctx.translateError('UNKNOWN_ERROR');

// check payment intent status (must be successful)
if (paymentIntent.status !== 'succeeded') {
// remove the Payment on our side if any that corresponds to this intent
const payment = await Payments.findOne({
user: ctx.state.user._id,
stripe_payment_intent_id: paymentIntent.id
});
if (payment) {
// remove the payment from our side
await payment.remove();
// find and save the associated user
// so that their plan_expires_at gets updated
await ctx.state.user.save();
}

throw ctx.translateError('INVALID_PAYMENT_INTENT');
}

if (paymentIntent.invoice) invoiceId = paymentIntent.invoice;

const paymentMethod = await stripe.paymentMethods.retrieve(
Expand Down Expand Up @@ -583,6 +602,24 @@ async function retrieveDomainBilling(ctx) {

if (!paymentIntent) throw ctx.translateError('UNKNOWN_ERROR');

// check payment intent status (must be successful)
if (paymentIntent.status !== 'succeeded') {
// remove the Payment on our side if any that corresponds to this intent
const payment = await Payments.findOne({
user: ctx.state.user._id,
stripe_payment_intent_id: paymentIntent.id
});
if (payment) {
// remove the payment from our side
await payment.remove();
// find and save the associated user
// so that their plan_expires_at gets updated
await ctx.state.user.save();
}

throw ctx.translateError('INVALID_PAYMENT_INTENT');
}

const paymentMethod = await stripe.paymentMethods.retrieve(
paymentIntent.payment_method
);
Expand Down Expand Up @@ -740,7 +777,8 @@ async function retrieveDomainBilling(ctx) {
stripe_invoice_id: invoiceId,
stripe_subscription_id: subscription?.id,
is_apple_pay: isApplePay,
is_google_pay: isGooglePay
is_google_pay: isGooglePay,
stack: new Error('stack').stack
});
// log the payment just for sanity
ctx.logger.info('stripe payment created', { payment });
Expand Down Expand Up @@ -1002,7 +1040,8 @@ async function retrieveDomainBilling(ctx) {
kind: 'one-time',
paypal_order_id: body.id,
paypal_transaction_id: transactionId,
invoice_at: now
invoice_at: now,
stack: new Error('stack').stack
});
// log the payment just for sanity
ctx.logger.info('paypal payment created', { payment });
Expand Down Expand Up @@ -1230,7 +1269,8 @@ async function retrieveDomainBilling(ctx) {
kind: 'subscription',
paypal_subscription_id: body.id,
paypal_transaction_id: transactionId,
invoice_at: now
invoice_at: now,
stack: new Error('stack').stack
});
// log the payment just for sanity
ctx.logger.info('paypal payment created', { payment });
Expand Down Expand Up @@ -1581,6 +1621,7 @@ async function retrieveDomainBilling(ctx) {
await Payments.create(
ctx.state.conversion[ctx.query.plan].map((payment) => ({
...payment,
stack: new Error('stack').stack,
invoice_at: now
}))
);
Expand Down
6 changes: 6 additions & 0 deletions app/models/payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ const i18n = require('#helpers/i18n');
const inline = pify(webResourceInliner.html);

const Payments = new mongoose.Schema({
//
// stack trace similar to `console.trace();`
// generated via `new Error().stack` which is useful for determining culprit if duplicate payments occur
// (or payments that were created that should not have been created)
//
stack: String,
user: {
type: mongoose.Schema.ObjectId,
ref: 'Users',
Expand Down
2 changes: 2 additions & 0 deletions config/phrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ module.exports = {
'You do not currently have an active subscription, or it was recently cancelled.',
SUBSCRIPTION_CANCELLED: 'You have successfully cancelled your subscription.',
ONE_TIME_PAYMENT_SUCCESSFUL: 'You have successfully made a one-time payment.',
INVALID_PAYMENT_INTENT:
'Payment was not successful and charge was not processed. Please try again or contact us for help.',
REFUND_ERROR_OCCURRED:
'An error occurred while processing refunds. We have been notified by email.',
REFUND_SUCCESSFUL:
Expand Down
3 changes: 2 additions & 1 deletion helpers/sync-paypal-subscription-payments-by-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ async function syncPayPalSubscriptionPaymentsByUser(errorEmails, customer) {
amount_refunded: amountRefunded,
[config.userFields.paypalSubscriptionID]: subscription.id,
paypal_transaction_id: transaction.id,
invoice_at: new Date(transaction.time)
invoice_at: new Date(transaction.time),
stack: new Error('stack').stack
};
logger.info('creating new payment');
await Payments.create(payment);
Expand Down
37 changes: 30 additions & 7 deletions helpers/sync-stripe-payment-intent.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,35 @@ function syncStripePaymentIntent(user) {
// eslint-disable-next-line complexity
return async function (errorEmails, paymentIntent) {
logger.info(`paymentIntent ${paymentIntent.id}`);

const q = {
user: user._id
};

try {
if (paymentIntent.status !== 'succeeded') return errorEmails;
//
// payment intent status could be "failed", "canceled", "processing"
// or other non-complete status and so we don't want to store this if wasn't successful
// (otherwise the user might get credit for something that wasn't successful)
//
if (paymentIntent.status !== 'succeeded') {
// remove the Payment on our side if any that corresponds to this intent
const payment = await Payments.findOne({
...q,
stripe_payment_intent_id: paymentIntent.id
});
if (payment) {
// remove the payment from our side
await payment.remove();
// find and save the associated user
// so that their plan_expires_at gets updated
const existingUser = await Users.findById(user._id);
if (!existingUser) throw new Error('User does not exist');
await existingUser.save();
}

return errorEmails;
}

//
// charges includes a `data` Array with only one charge (the latest/successful)
Expand Down Expand Up @@ -119,11 +146,6 @@ function syncStripePaymentIntent(user) {
// we attempt to look up the payment in our system, if it already exists
// we validate it and modify any missing params, if it doesnt, we create it
// depending on how it was created it will have some of the following fields

const q = {
user: user._id
};

let [payment, ...tooManyPayments] = await Payments.find({
...q,
stripe_payment_intent_id: paymentIntent.id
Expand Down Expand Up @@ -281,7 +303,8 @@ function syncStripePaymentIntent(user) {
stripe_payment_intent_id: paymentIntent?.id,
stripe_invoice_id: invoice?.id,
stripe_subscription_id: invoice?.subscription,
invoice_at: dayjs.unix(paymentIntent.created).toDate()
invoice_at: dayjs.unix(paymentIntent.created).toDate(),
stack: new Error('stack').stack
};

if (stripeCharge.payment_method_details.type === 'card') {
Expand Down
16 changes: 11 additions & 5 deletions jobs/billing.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function mapper(id) {
)
return;

// if every user domain was good, not disposable, and not restricted then return early
// ensure that user is the admin of at least one domain on a paid plan
const domains = await Domains.find({
'members.user': user._id
})
Expand All @@ -77,13 +77,19 @@ async function mapper(id) {
(member) => member.user.toString() === user.id
);
if (!member || member.group !== 'admin') continue;
const { isDisposable, isRestricted } = Domains.getNameRestrictions(
domain.name
);
if (isDisposable || isRestricted) {
if (domain.plan !== 'free') {
requiresPaidPlan = true;
break;
}

// NOTE: jobs/check-bad-domains takes care of this notification
// const { isDisposable, isRestricted } = Domains.getNameRestrictions(
// domain.name
// );
// if (isDisposable || isRestricted) {
// requiresPaidPlan = true;
// break;
// }
}

if (!requiresPaidPlan) return;
Expand Down
1 change: 1 addition & 0 deletions jobs/fix-missing-invoice-at.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async function mapper(id) {
if (!_.isDate(payment.created_at) || !_.isDate(payment.updated_at)) {
const clone = payment.toObject();
await payment.remove();
clone.stack = clone.stack || new Error('stack').stack;
payment = await Payments.create(clone);
}

Expand Down
9 changes: 2 additions & 7 deletions jobs/paypal/sync-paypal-subscription-payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,8 @@ async function syncPayPalSubscriptionPayments() {
[]
);

if (errorEmails.length > 0) {
try {
await Promise.all(errorEmails.map((email) => emailHelper(email)));
} catch (err) {
await logger.error(err);
}
}
if (errorEmails.length > 0)
await Promise.all(errorEmails.map((email) => emailHelper(email)));

await logger.info('Paypal subscriptions synced to payments');
}
Expand Down
11 changes: 3 additions & 8 deletions jobs/stripe/sync-stripe-payments.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,10 @@ async function syncStripePayments() {

await pMapSeries(stripeCustomers, mapper);

if (errorEmails.length > 0) {
try {
await Promise.all(errorEmails.map((email) => emailHelper(email)));
} catch (err) {
logger.error(err);
}
}
if (errorEmails.length > 0)
await Promise.all(errorEmails.map((email) => emailHelper(email)));

logger.info('Stripe payments synced successfully');
await logger.info('Stripe payments synced successfully');
}

module.exports = syncStripePayments;
3 changes: 2 additions & 1 deletion locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "من الرأس يجب أن يكون مساوياً لـ <span class=\"notranslate\">%s</span>",
"This header's value has": "قيمة هذا الرأس لها",
"header parsed addresses removed from it.": "تمت إزالة العناوين التي تم تحليلها منه.",
"We gave you free credit! Thank you for being a customer!": "لقد قدمنا لك رصيدا مجانيا! شكرا لكونك عميلا!"
"We gave you free credit! Thank you for being a customer!": "لقد قدمنا لك رصيدا مجانيا! شكرا لكونك عميلا!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "لم يكن الدفع ناجحًا ولم تتم معالجة الرسوم. يرجى المحاولة مرة أخرى أو الاتصال بنا للحصول على المساعدة."
}
3 changes: 2 additions & 1 deletion locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "Od záhlaví se musí rovnat <span class=\"notranslate\">%s</span>",
"This header's value has": "Hodnota této hlavičky má",
"header parsed addresses removed from it.": "z něj byly odstraněny analyzované adresy hlavičky.",
"We gave you free credit! Thank you for being a customer!": "Dali jsme vám kredit zdarma! Děkujeme, že jste zákazníkem!"
"We gave you free credit! Thank you for being a customer!": "Dali jsme vám kredit zdarma! Děkujeme, že jste zákazníkem!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Platba nebyla úspěšná a platba nebyla zpracována. Zkuste to znovu nebo nás kontaktujte s žádostí o pomoc."
}
3 changes: 2 additions & 1 deletion locales/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -4544,5 +4544,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "Fra-header skal være lig med <span class=\"notranslate\">%s</span>",
"This header's value has": "Denne overskrifts værdi har",
"header parsed addresses removed from it.": "header parsede adresser fjernet fra den.",
"We gave you free credit! Thank you for being a customer!": "Vi gav dig gratis kredit! Tak fordi du er kunde!"
"We gave you free credit! Thank you for being a customer!": "Vi gav dig gratis kredit! Tak fordi du er kunde!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Betalingen lykkedes ikke, og debiteringen blev ikke behandlet. Prøv venligst igen, eller kontakt os for at få hjælp."
}
3 changes: 2 additions & 1 deletion locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3836,5 +3836,6 @@
"We will automatically send you receipts by email.": "Wir senden Ihnen automatisch Quittungen per E-Mail zu.",
"Discount": "Rabatt",
"We gave you free credit! Thank you for being a customer!": "Wir haben Ihnen kostenloses Guthaben geschenkt! Vielen Dank, dass Sie Kunde sind!",
"You earned free credit!": "Sie haben kostenloses Guthaben erhalten!"
"You earned free credit!": "Sie haben kostenloses Guthaben erhalten!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Die Zahlung war nicht erfolgreich und die Belastung wurde nicht verarbeitet. Bitte versuchen Sie es erneut oder kontaktieren Sie uns für Hilfe."
}
3 changes: 2 additions & 1 deletion locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -4806,5 +4806,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "Desde el encabezado debe ser igual a <span class=\"notranslate\">%s</span>",
"This header's value has": "El valor de este encabezado tiene",
"header parsed addresses removed from it.": "direcciones analizadas del encabezado eliminadas del mismo.",
"We gave you free credit! Thank you for being a customer!": "¡Te dimos crédito gratis! ¡Gracias por ser cliente!"
"We gave you free credit! Thank you for being a customer!": "¡Te dimos crédito gratis! ¡Gracias por ser cliente!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "El pago no se realizó correctamente y el cargo no se procesó. Inténtelo de nuevo o contáctenos para obtener ayuda."
}
3 changes: 2 additions & 1 deletion locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4653,5 +4653,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "Otsikon on oltava yhtä suuri kuin <span class=\"notranslate\">%s</span>",
"This header's value has": "Tämän otsikon arvo on",
"header parsed addresses removed from it.": "header jäsennetyt osoitteet poistettu siitä.",
"We gave you free credit! Thank you for being a customer!": "Annoimme sinulle ilmaista luottoa! Kiitos, että olet asiakas!"
"We gave you free credit! Thank you for being a customer!": "Annoimme sinulle ilmaista luottoa! Kiitos, että olet asiakas!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Maksu ei onnistunut eikä veloitusta käsitelty. Yritä uudelleen tai ota meihin yhteyttä saadaksesi apua."
}
3 changes: 2 additions & 1 deletion locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "L&#39;en-tête From doit être égal à <span class=\"notranslate\">%s</span>",
"This header's value has": "La valeur de cet en-tête a",
"header parsed addresses removed from it.": "l'en-tête a analysé les adresses qui en ont été supprimées.",
"We gave you free credit! Thank you for being a customer!": "Nous vous avons offert un crédit gratuit ! Merci d'être client !"
"We gave you free credit! Thank you for being a customer!": "Nous vous avons offert un crédit gratuit ! Merci d'être client !",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Le paiement n'a pas abouti et les frais n'ont pas été traités. Veuillez réessayer ou contactez-nous pour obtenir de l'aide."
}
3 changes: 2 additions & 1 deletion locales/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "From header חייב להיות שווה ל- <span class=\"notranslate\">%s</span>",
"This header's value has": "יש לערך של כותרת זו",
"header parsed addresses removed from it.": "כתובות מנותחות בכותרת הוסרו ממנה.",
"We gave you free credit! Thank you for being a customer!": "נתנו לך אשראי חינם! תודה שהיית לקוח!"
"We gave you free credit! Thank you for being a customer!": "נתנו לך אשראי חינם! תודה שהיית לקוח!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "התשלום לא הצליח והחיוב לא עובד. אנא נסה שוב או פנה אלינו לקבלת עזרה."
}
3 changes: 2 additions & 1 deletion locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "A fejléctől egyenlőnek kell lennie: <span class=\"notranslate\">%s</span>",
"This header's value has": "Ennek a fejlécnek az értéke",
"header parsed addresses removed from it.": "fejléc elemzett címek eltávolítása belőle.",
"We gave you free credit! Thank you for being a customer!": "Ingyenes hitelt adtunk! Köszönjük, hogy ügyfelünk vagy!"
"We gave you free credit! Thank you for being a customer!": "Ingyenes hitelt adtunk! Köszönjük, hogy ügyfelünk vagy!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "A fizetés nem volt sikeres, és a terhelés feldolgozása nem történt meg. Kérjük, próbálja újra, vagy forduljon hozzánk segítségért."
}
3 changes: 2 additions & 1 deletion locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "Dari header harus sama dengan <span class=\"notranslate\">%s</span>",
"This header's value has": "Nilai tajuk ini miliki",
"header parsed addresses removed from it.": "alamat header yang diurai dihapus darinya.",
"We gave you free credit! Thank you for being a customer!": "Kami memberi Anda kredit gratis! Terima kasih telah menjadi pelanggan!"
"We gave you free credit! Thank you for being a customer!": "Kami memberi Anda kredit gratis! Terima kasih telah menjadi pelanggan!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Pembayaran tidak berhasil dan tagihan tidak diproses. Silakan coba lagi atau hubungi kami untuk bantuan."
}
3 changes: 2 additions & 1 deletion locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -4808,5 +4808,6 @@
"From header must be equal to <span class=\"notranslate\">%s</span>": "L&#39;intestazione From deve essere uguale a <span class=\"notranslate\">%s</span>",
"This header's value has": "Il valore di questa intestazione ha",
"header parsed addresses removed from it.": "indirizzi analizzati dall'intestazione rimossi da esso.",
"We gave you free credit! Thank you for being a customer!": "Ti abbiamo dato credito gratis! Grazie per essere un cliente!"
"We gave you free credit! Thank you for being a customer!": "Ti abbiamo dato credito gratis! Grazie per essere un cliente!",
"Payment was not successful and charge was not processed. Please try again or contact us for help.": "Il pagamento non è andato a buon fine e l'addebito non è stato elaborato. Riprova o contattaci per ricevere assistenza."
}
Loading

0 comments on commit 83fa56b

Please sign in to comment.