Added donation message to Stripe and Email (#20828)
ref PLG-160 - Refactored donation handling logic to be processed within the `checkout.session.completed` webhook event. - Added support for capturing and storing donation messages from Stripe sessions. - Integrated donation messages into the email notifications sent to staff. - Added database integration. - Removed redundant donation logic from the invoice.payment_succeeded webhook, since custom fields isn't supported. - Updated and added new tests --------- Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
This commit is contained in:
parent
32edc12cc2
commit
e8e1b8ea2f
@ -11,3 +11,9 @@ Object {
|
|||||||
"url": "https://checkout.stripe.com/c/pay/fake-data",
|
"url": "https://checkout.stripe.com/c/pay/fake-data",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Create Stripe Checkout Session for Donations check if donation message is in email 1: [body] 1`] = `
|
||||||
|
Object {
|
||||||
|
"url": "https://checkout.stripe.com/c/pay/fake-data",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -58,21 +58,29 @@ describe('Create Stripe Checkout Session for Donations', function () {
|
|||||||
.expectStatus(200)
|
.expectStatus(200)
|
||||||
.matchBodySnapshot();
|
.matchBodySnapshot();
|
||||||
|
|
||||||
// Send a webhook of a paid invoice for this session
|
// Send a webhook of a completed checkout session for this donation
|
||||||
await stripeMocker.sendWebhook({
|
await stripeMocker.sendWebhook({
|
||||||
type: 'invoice.payment_succeeded',
|
type: 'checkout.session.completed',
|
||||||
data: {
|
data: {
|
||||||
object: {
|
object: {
|
||||||
type: 'invoice',
|
mode: 'payment',
|
||||||
paid: true,
|
amount_total: 1200,
|
||||||
amount_paid: 1200,
|
|
||||||
currency: 'usd',
|
currency: 'usd',
|
||||||
customer: (stripeMocker.checkoutSessions[0].customer),
|
customer: (stripeMocker.checkoutSessions[0].customer),
|
||||||
customer_name: 'Paid Test',
|
customer_details: {
|
||||||
customer_email: 'exampledonation@example.com',
|
name: 'Paid Test',
|
||||||
|
email: 'exampledonation@example.com'
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
...(stripeMocker.checkoutSessions[0].invoice_creation?.invoice_data?.metadata ?? {})
|
...(stripeMocker.checkoutSessions[0].metadata ?? {}),
|
||||||
|
ghost_donation: true
|
||||||
|
},
|
||||||
|
custom_fields: [{
|
||||||
|
key: 'donation_message',
|
||||||
|
text: {
|
||||||
|
value: 'You are the best! Have a lovely day!'
|
||||||
}
|
}
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -87,11 +95,13 @@ describe('Create Stripe Checkout Session for Donations', function () {
|
|||||||
const lastDonation = await models.DonationPaymentEvent.findOne({
|
const lastDonation = await models.DonationPaymentEvent.findOne({
|
||||||
email: 'exampledonation@example.com'
|
email: 'exampledonation@example.com'
|
||||||
}, {require: true});
|
}, {require: true});
|
||||||
|
|
||||||
assert.equal(lastDonation.get('amount'), 1200);
|
assert.equal(lastDonation.get('amount'), 1200);
|
||||||
assert.equal(lastDonation.get('currency'), 'usd');
|
assert.equal(lastDonation.get('currency'), 'usd');
|
||||||
assert.equal(lastDonation.get('email'), 'exampledonation@example.com');
|
assert.equal(lastDonation.get('email'), 'exampledonation@example.com');
|
||||||
assert.equal(lastDonation.get('name'), 'Paid Test');
|
assert.equal(lastDonation.get('name'), 'Paid Test');
|
||||||
assert.equal(lastDonation.get('member_id'), null);
|
assert.equal(lastDonation.get('member_id'), null);
|
||||||
|
assert.equal(lastDonation.get('donation_message'), 'You are the best! Have a lovely day!');
|
||||||
|
|
||||||
// Check referrer
|
// Check referrer
|
||||||
assert.equal(lastDonation.get('referrer_url'), 'example.com');
|
assert.equal(lastDonation.get('referrer_url'), 'example.com');
|
||||||
@ -125,6 +135,7 @@ describe('Create Stripe Checkout Session for Donations', function () {
|
|||||||
|
|
||||||
await membersAgent.post('/api/create-stripe-checkout-session/')
|
await membersAgent.post('/api/create-stripe-checkout-session/')
|
||||||
.body({
|
.body({
|
||||||
|
mode: 'payment',
|
||||||
customerEmail: email,
|
customerEmail: email,
|
||||||
identity: token,
|
identity: token,
|
||||||
type: 'donation',
|
type: 'donation',
|
||||||
@ -146,21 +157,29 @@ describe('Create Stripe Checkout Session for Donations', function () {
|
|||||||
.expectStatus(200)
|
.expectStatus(200)
|
||||||
.matchBodySnapshot();
|
.matchBodySnapshot();
|
||||||
|
|
||||||
// Send a webhook of a paid invoice for this session
|
// Send a webhook of a completed checkout session for this donation
|
||||||
await stripeMocker.sendWebhook({
|
await stripeMocker.sendWebhook({
|
||||||
type: 'invoice.payment_succeeded',
|
type: 'checkout.session.completed',
|
||||||
data: {
|
data: {
|
||||||
object: {
|
object: {
|
||||||
type: 'invoice',
|
mode: 'payment',
|
||||||
paid: true,
|
amount_total: 1220,
|
||||||
amount_paid: 1220,
|
|
||||||
currency: 'eur',
|
currency: 'eur',
|
||||||
customer: (stripeMocker.checkoutSessions[0].customer),
|
customer: (stripeMocker.checkoutSessions[0].customer),
|
||||||
customer_name: 'Member Test',
|
customer_details: {
|
||||||
customer_email: email,
|
name: 'Member Test',
|
||||||
|
email: email
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
...(stripeMocker.checkoutSessions[0].invoice_creation?.invoice_data?.metadata ?? {})
|
...(stripeMocker.checkoutSessions[0].metadata ?? {}),
|
||||||
|
ghost_donation: true
|
||||||
|
},
|
||||||
|
custom_fields: [{
|
||||||
|
key: 'donation_message',
|
||||||
|
text: {
|
||||||
|
value: 'You are the best! Have a lovely day!'
|
||||||
}
|
}
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -180,6 +199,7 @@ describe('Create Stripe Checkout Session for Donations', function () {
|
|||||||
assert.equal(lastDonation.get('email'), email);
|
assert.equal(lastDonation.get('email'), email);
|
||||||
assert.equal(lastDonation.get('name'), 'Member Test');
|
assert.equal(lastDonation.get('name'), 'Member Test');
|
||||||
assert.equal(lastDonation.get('member_id'), member.id);
|
assert.equal(lastDonation.get('member_id'), member.id);
|
||||||
|
assert.equal(lastDonation.get('donation_message'), 'You are the best! Have a lovely day!');
|
||||||
|
|
||||||
// Check referrer
|
// Check referrer
|
||||||
assert.equal(lastDonation.get('referrer_url'), 'example.com');
|
assert.equal(lastDonation.get('referrer_url'), 'example.com');
|
||||||
@ -191,4 +211,74 @@ describe('Create Stripe Checkout Session for Donations', function () {
|
|||||||
assert.equal(lastDonation.get('attribution_type'), 'post');
|
assert.equal(lastDonation.get('attribution_type'), 'post');
|
||||||
assert.equal(lastDonation.get('attribution_url'), url);
|
assert.equal(lastDonation.get('attribution_url'), url);
|
||||||
});
|
});
|
||||||
|
it('check if donation message is in email', async function () {
|
||||||
|
const post = await getPost(fixtureManager.get('posts', 0).id);
|
||||||
|
const url = urlService.getUrlByResourceId(post.id, {absolute: false});
|
||||||
|
|
||||||
|
await membersAgent.post('/api/create-stripe-checkout-session/')
|
||||||
|
.body({
|
||||||
|
mode: 'payment',
|
||||||
|
type: 'donation',
|
||||||
|
customerEmail: 'paid@test.com',
|
||||||
|
successUrl: 'https://example.com/?type=success',
|
||||||
|
cancelUrl: 'https://example.com/?type=cancel',
|
||||||
|
metadata: {
|
||||||
|
urlHistory: [
|
||||||
|
{
|
||||||
|
path: url,
|
||||||
|
time: Date.now(),
|
||||||
|
referrerMedium: null,
|
||||||
|
referrerSource: 'ghost-explore',
|
||||||
|
referrerUrl: 'https://example.com/blog/'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
ghost_donation: true
|
||||||
|
},
|
||||||
|
custom_fields: [{
|
||||||
|
key: 'donation_message',
|
||||||
|
label: {
|
||||||
|
type: 'custom',
|
||||||
|
custom: 'Add a personal note'
|
||||||
|
},
|
||||||
|
type: 'text',
|
||||||
|
optional: true
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot();
|
||||||
|
|
||||||
|
// Send a webhook of a completed checkout session for this donation
|
||||||
|
await stripeMocker.sendWebhook({
|
||||||
|
type: 'checkout.session.completed',
|
||||||
|
data: {
|
||||||
|
object: {
|
||||||
|
mode: 'payment',
|
||||||
|
amount_total: 1200,
|
||||||
|
currency: 'usd',
|
||||||
|
customer: (stripeMocker.checkoutSessions[0].customer),
|
||||||
|
customer_details: {
|
||||||
|
name: 'Paid Test',
|
||||||
|
email: 'exampledonation@example.com'
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
...(stripeMocker.checkoutSessions[0].metadata ?? {}),
|
||||||
|
ghost_donation: true
|
||||||
|
},
|
||||||
|
custom_fields: [{
|
||||||
|
key: 'donation_message',
|
||||||
|
text: {
|
||||||
|
value: 'You are the best! Have a lovely day!'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if donation message is in email
|
||||||
|
mockManager.assert.sentEmail({
|
||||||
|
subject: '💰 One-time payment received: $12.00 from Paid Test',
|
||||||
|
to: 'jbloggs@example.com',
|
||||||
|
text: /You are the best! Have a lovely day!/
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@ type DonationEventModelInstance = BookshelfModelInstance & {
|
|||||||
member_id: string | null;
|
member_id: string | null;
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
donation_message: string | null;
|
||||||
|
|
||||||
attribution_id: string | null;
|
attribution_id: string | null;
|
||||||
attribution_url: string | null;
|
attribution_url: string | null;
|
||||||
@ -36,6 +37,7 @@ export class DonationBookshelfRepository implements DonationRepository {
|
|||||||
member_id: event.memberId,
|
member_id: event.memberId,
|
||||||
amount: event.amount,
|
amount: event.amount,
|
||||||
currency: event.currency,
|
currency: event.currency,
|
||||||
|
donation_message: event.donationMessage,
|
||||||
|
|
||||||
attribution_id: event.attributionId,
|
attribution_id: event.attributionId,
|
||||||
attribution_url: event.attributionUrl,
|
attribution_url: event.attributionUrl,
|
||||||
|
@ -5,6 +5,7 @@ export class DonationPaymentEvent {
|
|||||||
memberId: string | null;
|
memberId: string | null;
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
|
donationMessage: string | null;
|
||||||
|
|
||||||
attributionId: string | null;
|
attributionId: string | null;
|
||||||
attributionUrl: string | null;
|
attributionUrl: string | null;
|
||||||
@ -21,6 +22,7 @@ export class DonationPaymentEvent {
|
|||||||
this.memberId = data.memberId;
|
this.memberId = data.memberId;
|
||||||
this.amount = data.amount;
|
this.amount = data.amount;
|
||||||
this.currency = data.currency;
|
this.currency = data.currency;
|
||||||
|
this.donationMessage = data.donationMessage;
|
||||||
|
|
||||||
this.attributionId = data.attributionId;
|
this.attributionId = data.attributionId;
|
||||||
this.attributionUrl = data.attributionUrl;
|
this.attributionUrl = data.attributionUrl;
|
||||||
|
@ -296,7 +296,8 @@ class StaffServiceEmails {
|
|||||||
donation: {
|
donation: {
|
||||||
name: donationPaymentEvent.name ?? donationPaymentEvent.email,
|
name: donationPaymentEvent.name ?? donationPaymentEvent.email,
|
||||||
email: donationPaymentEvent.email,
|
email: donationPaymentEvent.email,
|
||||||
amount: formattedAmount
|
amount: formattedAmount,
|
||||||
|
donationMessage: donationPaymentEvent.donationMessage
|
||||||
},
|
},
|
||||||
memberData,
|
memberData,
|
||||||
accentColor: this.settingsCache.get('accent_color')
|
accentColor: this.settingsCache.get('accent_color')
|
||||||
|
@ -28,26 +28,72 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
||||||
<h1 style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 26px; color: #15212A; font-weight: bold; line-height: 28px; margin: 0; padding-bottom: 24px;">Cha-ching! You received a {{donation.amount}} tip from {{donation.name}}.</h1>
|
<h1 style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 26px; color: #15212A; font-weight: bold; line-height: 28px; margin: 0; padding-bottom: 24px;">Cha-ching! You received a tip.</h1>
|
||||||
<table width="100" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F4F5F6; border-radius: 8px;">
|
<table width="100" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F4F5F6; border-radius: 8px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="padding: 24px; text-align: center;">
|
<td align="left" style="padding: 24px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
|
||||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">From:</p>
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">From:</p>
|
||||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; text-align: left; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 700;">{{donation.name}} (<span style="color:{{accentColor}}; text-decoration: none;">{{donation.email}}</span>)</p>
|
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.name}} (<span style="color:{{accentColor}}; text-decoration: none;">{{donation.email}}</span>)</p>
|
||||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Amount received:</p>
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Amount received:</p>
|
||||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; line-height: 26px; padding: 0; padding-bottom: 24px; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
|
||||||
|
{{#if donation.donationMessage}}
|
||||||
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 22px; padding: 0; padding-bottom: 28px; text-align: left; margin: 0; color: #15171A; font-weight: 400;">“{{donation.donationMessage}}”</p>
|
||||||
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||||
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:0 24px 24px;">
|
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
{{#if donation.donationMessage}}
|
||||||
|
<td align="left" style="padding: 0;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; vertical-align: top; background-color: {{accentColor}}; border-radius: 8px; text-align: center;">
|
||||||
|
<a href="mailto:{{donation.email}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 8px; padding: 10px 20px; text-decoration: none;">Reply</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td align="left" style="padding: 0; padding-left: 8px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; vertical-align: top; background-color: #E5E9ED; border-radius: 8px; text-align: center;">
|
||||||
{{#if memberData}}
|
{{#if memberData}}
|
||||||
<a href="{{memberData.adminUrl}}" style="border:solid 1px {{accentColor}};border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:{{accentColor}};border-color:{{accentColor}};color:#ffffff">View member</a>
|
<a href="{{memberData.adminUrl}}" target="_blank" style="display: inline-block; color: #15171A; background-color: #E5E9ED; border: solid 1px #E5E9ED; border-radius: 8px; padding: 10px 20px; text-decoration: none;">View member</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="{{adminUrl}}" style="border:solid 1px {{accentColor}};border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:{{accentColor}};border-color:{{accentColor}};color:#ffffff">View dashboard</a>
|
<a href="{{adminUrl}}" target="_blank" style="display: inline-block; color: #15171A; background-color: #E5E9ED; border: solid 1px #E5E9ED; border-radius: 8px; padding: 10px 20px; text-decoration: none;">View dashboard</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
{{else}}
|
||||||
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; vertical-align: top; background-color: {{accentColor}}; border-radius: 8px; text-align: center;">
|
||||||
|
{{#if memberData}}
|
||||||
|
<a href="{{memberData.adminUrl}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 8px; padding: 10px 20px; text-decoration: none;">View member</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="{{adminUrl}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 8px; padding: 10px 20px; text-decoration: none;">View dashboard</a>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ Cha-ching!
|
|||||||
|
|
||||||
You received a one-time payment from of ${data.donation.amount} from "${data.donation.name}".
|
You received a one-time payment from of ${data.donation.amount} from "${data.donation.name}".
|
||||||
|
|
||||||
|
Message: ${data.donation.donationMessage ? data.donation.donationMessage : 'No message provided'}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Sent to ${data.toEmail} from ${data.siteDomain}.
|
Sent to ${data.toEmail} from ${data.siteDomain}.
|
||||||
|
@ -930,7 +930,8 @@ describe('StaffService', function () {
|
|||||||
amount: 1500,
|
amount: 1500,
|
||||||
currency: 'eur',
|
currency: 'eur',
|
||||||
name: 'Simon',
|
name: 'Simon',
|
||||||
email: 'simon@example.com'
|
email: 'simon@example.com',
|
||||||
|
donationMessage: 'Thank you for the awesome newsletter!'
|
||||||
};
|
};
|
||||||
|
|
||||||
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
||||||
@ -943,6 +944,112 @@ describe('StaffService', function () {
|
|||||||
sinon.match.has('html', sinon.match('One-time payment received: €15.00 from Simon'))
|
sinon.match.has('html', sinon.match('One-time payment received: €15.00 from Simon'))
|
||||||
).should.be.true();
|
).should.be.true();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has donation message in text', async function () {
|
||||||
|
const donationPaymentEvent = {
|
||||||
|
amount: 1500,
|
||||||
|
currency: 'eur',
|
||||||
|
name: 'Jamie',
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
donationMessage: 'Thank you for the awesome newsletter!'
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
||||||
|
|
||||||
|
getEmailAlertUsersStub.calledWith('donation').should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledWith(
|
||||||
|
sinon.match.has('text', sinon.match('Thank you for the awesome newsletter!'))
|
||||||
|
).should.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has donation message in html', async function () {
|
||||||
|
const donationPaymentEvent = {
|
||||||
|
amount: 1500,
|
||||||
|
currency: 'eur',
|
||||||
|
name: 'Jamie',
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
donationMessage: 'Thank you for the awesome newsletter!'
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
||||||
|
|
||||||
|
getEmailAlertUsersStub.calledWith('donation').should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledWith(
|
||||||
|
sinon.match.has('html', sinon.match('Thank you for the awesome newsletter!'))
|
||||||
|
).should.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain donation message in HTML if not provided', async function () {
|
||||||
|
const donationPaymentEvent = {
|
||||||
|
amount: 1500,
|
||||||
|
currency: 'eur',
|
||||||
|
name: 'Jamie',
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
donationMessage: null // No donation message provided
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
||||||
|
|
||||||
|
getEmailAlertUsersStub.calledWith('donation').should.be.true();
|
||||||
|
mailStub.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
// Check that the specific HTML block for the donation message is NOT present
|
||||||
|
mailStub.calledWith(
|
||||||
|
sinon.match.has('html', sinon.match(function (html) {
|
||||||
|
// Ensure that the block with `{{donation.donationMessage}}` does not exist in the rendered HTML
|
||||||
|
return !html.includes('“') && !html.includes('”');
|
||||||
|
}))
|
||||||
|
).should.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not really a relevant test, but it's here to show that the donation message is wrapped in quotation marks
|
||||||
|
// and that the above test is actually working, since only the donation message is wrapped in quotation marks
|
||||||
|
it('The donation message is wrapped in quotation marks', async function () {
|
||||||
|
const donationPaymentEvent = {
|
||||||
|
amount: 1500,
|
||||||
|
currency: 'eur',
|
||||||
|
name: 'Jamie',
|
||||||
|
email: 'jamie@example.com',
|
||||||
|
donationMessage: 'Thank you for the great newsletter!'
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
||||||
|
|
||||||
|
getEmailAlertUsersStub.calledWith('donation').should.be.true();
|
||||||
|
mailStub.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledWith(
|
||||||
|
sinon.match.has('html', sinon.match(function (html) {
|
||||||
|
return html.includes('“') && html.includes('”');
|
||||||
|
}))
|
||||||
|
).should.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send donation email without message', async function () {
|
||||||
|
const donationPaymentEvent = {
|
||||||
|
amount: 1500,
|
||||||
|
currency: 'eur',
|
||||||
|
name: 'Ronald',
|
||||||
|
email: 'ronald@example.com',
|
||||||
|
donationMessage: null
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.emails.notifyDonationReceived({donationPaymentEvent});
|
||||||
|
|
||||||
|
getEmailAlertUsersStub.calledWith('donation').should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledOnce.should.be.true();
|
||||||
|
|
||||||
|
mailStub.calledWith(
|
||||||
|
sinon.match.has('text', sinon.match('No message provided'))
|
||||||
|
).should.be.true();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('renderText for webmentions', function () {
|
describe('renderText for webmentions', function () {
|
||||||
|
@ -523,6 +523,14 @@ module.exports = class StripeAPI {
|
|||||||
/**
|
/**
|
||||||
* @type {Stripe.Checkout.SessionCreateParams}
|
* @type {Stripe.Checkout.SessionCreateParams}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO - add it higher up the stack to the metadata object.
|
||||||
|
// add ghost_donation key to metadata object
|
||||||
|
metadata = {
|
||||||
|
ghost_donation: true,
|
||||||
|
...metadata
|
||||||
|
};
|
||||||
|
|
||||||
const stripeSessionOptions = {
|
const stripeSessionOptions = {
|
||||||
mode: 'payment',
|
mode: 'payment',
|
||||||
success_url: successUrl || this._config.checkoutSessionSuccessUrl,
|
success_url: successUrl || this._config.checkoutSessionSuccessUrl,
|
||||||
@ -547,7 +555,18 @@ module.exports = class StripeAPI {
|
|||||||
line_items: [{
|
line_items: [{
|
||||||
price: priceId,
|
price: priceId,
|
||||||
quantity: 1
|
quantity: 1
|
||||||
}]
|
}],
|
||||||
|
custom_fields: [
|
||||||
|
{
|
||||||
|
key: 'donation_message',
|
||||||
|
label: {
|
||||||
|
type: 'custom',
|
||||||
|
custom: 'Add a personal note'
|
||||||
|
},
|
||||||
|
type: 'text',
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (customer && this._config.enableAutomaticTax) {
|
if (customer && this._config.enableAutomaticTax) {
|
||||||
|
@ -111,35 +111,8 @@ module.exports = class WebhookController {
|
|||||||
async invoiceEvent(invoice) {
|
async invoiceEvent(invoice) {
|
||||||
if (!invoice.subscription) {
|
if (!invoice.subscription) {
|
||||||
// Check if this is a one time payment, related to a donation
|
// Check if this is a one time payment, related to a donation
|
||||||
if (invoice.metadata.ghost_donation && invoice.paid) {
|
// this is being handled in checkoutSessionEvent because we need to handle the custom donation message
|
||||||
// Track a one time payment event
|
// which is not available in the invoice object
|
||||||
const amount = invoice.amount_paid;
|
|
||||||
|
|
||||||
const member = invoice.customer ? (await this.deps.memberRepository.get({
|
|
||||||
customer_id: invoice.customer
|
|
||||||
})) : null;
|
|
||||||
|
|
||||||
const data = DonationPaymentEvent.create({
|
|
||||||
name: member?.get('name') ?? invoice.customer_name,
|
|
||||||
email: member?.get('email') ?? invoice.customer_email,
|
|
||||||
memberId: member?.id ?? null,
|
|
||||||
amount,
|
|
||||||
currency: invoice.currency,
|
|
||||||
|
|
||||||
// Attribution data
|
|
||||||
attributionId: invoice.metadata.attribution_id ?? null,
|
|
||||||
attributionUrl: invoice.metadata.attribution_url ?? null,
|
|
||||||
attributionType: invoice.metadata.attribution_type ?? null,
|
|
||||||
referrerSource: invoice.metadata.referrer_source ?? null,
|
|
||||||
referrerMedium: invoice.metadata.referrer_medium ?? null,
|
|
||||||
referrerUrl: invoice.metadata.referrer_url ?? null
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.deps.donationRepository.save(data);
|
|
||||||
await this.deps.staffServiceEmails.notifyDonationReceived({
|
|
||||||
donationPaymentEvent: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const subscription = await this.api.getSubscription(invoice.subscription, {
|
const subscription = await this.api.getSubscription(invoice.subscription, {
|
||||||
@ -182,6 +155,40 @@ module.exports = class WebhookController {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async checkoutSessionEvent(session) {
|
async checkoutSessionEvent(session) {
|
||||||
|
if (session.mode === 'payment' && session.metadata?.ghost_donation) {
|
||||||
|
const donationField = session.custom_fields?.find(obj => obj?.key === 'donation_message');
|
||||||
|
// const customMessage = donationField?.text?.value ?? '';
|
||||||
|
|
||||||
|
// custom message should be null if it's empty
|
||||||
|
|
||||||
|
const donationMessage = donationField?.text?.value ? donationField.text.value : null;
|
||||||
|
|
||||||
|
const amount = session.amount_total;
|
||||||
|
const currency = session.currency;
|
||||||
|
const member = session.customer ? (await this.deps.memberRepository.get({
|
||||||
|
customer_id: session.customer
|
||||||
|
})) : null;
|
||||||
|
|
||||||
|
const data = DonationPaymentEvent.create({
|
||||||
|
name: member?.get('name') ?? session.customer_details.name,
|
||||||
|
email: member?.get('email') ?? session.customer_details.email,
|
||||||
|
memberId: member?.id ?? null,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
donationMessage,
|
||||||
|
attributionId: session.metadata.attribution_id ?? null,
|
||||||
|
attributionUrl: session.metadata.attribution_url ?? null,
|
||||||
|
attributionType: session.metadata.attribution_type ?? null,
|
||||||
|
referrerSource: session.metadata.referrer_source ?? null,
|
||||||
|
referrerMedium: session.metadata.referrer_medium ?? null,
|
||||||
|
referrerUrl: session.metadata.referrer_url ?? null
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.deps.donationRepository.save(data);
|
||||||
|
await this.deps.staffServiceEmails.notifyDonationReceived({
|
||||||
|
donationPaymentEvent: data
|
||||||
|
});
|
||||||
|
}
|
||||||
if (session.mode === 'setup') {
|
if (session.mode === 'setup') {
|
||||||
const setupIntent = await this.api.getSetupIntent(session.setup_intent);
|
const setupIntent = await this.api.getSetupIntent(session.setup_intent);
|
||||||
const member = await this.deps.memberRepository.get({
|
const member = await this.deps.memberRepository.get({
|
||||||
|
@ -508,8 +508,7 @@ describe('StripeAPI', function () {
|
|||||||
|
|
||||||
it('passes metadata correctly', async function () {
|
it('passes metadata correctly', async function () {
|
||||||
const metadata = {
|
const metadata = {
|
||||||
key1: 'value1',
|
ghost_donation: true
|
||||||
key2: 'value2'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await api.createDonationCheckoutSession({
|
await api.createDonationCheckoutSession({
|
||||||
@ -524,6 +523,56 @@ describe('StripeAPI', function () {
|
|||||||
should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.metadata);
|
should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.metadata);
|
||||||
should.deepEqual(mockStripe.checkout.sessions.create.firstCall.firstArg.metadata, metadata);
|
should.deepEqual(mockStripe.checkout.sessions.create.firstCall.firstArg.metadata, metadata);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes custom fields correctly', async function () {
|
||||||
|
await api.createDonationCheckoutSession({
|
||||||
|
priceId: 'priceId',
|
||||||
|
successUrl: '/success',
|
||||||
|
cancelUrl: '/cancel',
|
||||||
|
metadata: {},
|
||||||
|
customer: null,
|
||||||
|
customerEmail: mockCustomerEmail
|
||||||
|
});
|
||||||
|
|
||||||
|
should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.custom_fields);
|
||||||
|
const customFields = mockStripe.checkout.sessions.create.firstCall.firstArg.custom_fields;
|
||||||
|
should.equal(customFields.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct data for custom field message', async function () {
|
||||||
|
await api.createDonationCheckoutSession({
|
||||||
|
priceId: 'priceId',
|
||||||
|
successUrl: '/success',
|
||||||
|
cancelUrl: '/cancel',
|
||||||
|
metadata: {},
|
||||||
|
customer: null,
|
||||||
|
customerEmail: mockCustomerEmail
|
||||||
|
});
|
||||||
|
|
||||||
|
const customFields = mockStripe.checkout.sessions.create.firstCall.firstArg.custom_fields;
|
||||||
|
should.deepEqual(customFields[0], {
|
||||||
|
key: 'donation_message',
|
||||||
|
label: {
|
||||||
|
type: 'custom',
|
||||||
|
custom: 'Add a personal note'
|
||||||
|
},
|
||||||
|
type: 'text',
|
||||||
|
optional: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not have more than 3 custom fields (stripe limitation)', async function () {
|
||||||
|
await api.createDonationCheckoutSession({
|
||||||
|
priceId: 'priceId',
|
||||||
|
successUrl: '/success',
|
||||||
|
cancelUrl: '/cancel',
|
||||||
|
metadata: {},
|
||||||
|
customer: null,
|
||||||
|
customerEmail: mockCustomerEmail
|
||||||
|
});
|
||||||
|
|
||||||
|
should.ok(mockStripe.checkout.sessions.create.firstCall.firstArg.custom_fields.length <= 3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
174
ghost/stripe/test/unit/lib/WebhookController.test.js
Normal file
174
ghost/stripe/test/unit/lib/WebhookController.test.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
const chai = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const {expect} = chai;
|
||||||
|
const WebhookController = require('../../../lib/WebhookController');
|
||||||
|
// const {DonationPaymentEvent} = require('@tryghost/donations');
|
||||||
|
|
||||||
|
describe('WebhookController', function () {
|
||||||
|
let controller;
|
||||||
|
let deps;
|
||||||
|
let req;
|
||||||
|
let res;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
deps = {
|
||||||
|
api: {getSubscription: sinon.stub(), getCustomer: sinon.stub(), getSetupIntent: sinon.stub(), attachPaymentMethodToCustomer: sinon.stub(), updateSubscriptionDefaultPaymentMethod: sinon.stub()},
|
||||||
|
webhookManager: {parseWebhook: sinon.stub()},
|
||||||
|
eventRepository: {registerPayment: sinon.stub()},
|
||||||
|
memberRepository: {get: sinon.stub(), create: sinon.stub(), update: sinon.stub(), linkSubscription: sinon.stub(), upsertCustomer: sinon.stub()},
|
||||||
|
donationRepository: {save: sinon.stub()},
|
||||||
|
productRepository: {get: sinon.stub()},
|
||||||
|
staffServiceEmails: {notifyDonationReceived: sinon.stub()},
|
||||||
|
sendSignupEmail: sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
|
controller = new WebhookController(deps);
|
||||||
|
|
||||||
|
req = {
|
||||||
|
body: {},
|
||||||
|
headers: {
|
||||||
|
'stripe-signature': 'valid-signature'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
res = {
|
||||||
|
writeHead: sinon.stub(),
|
||||||
|
end: sinon.stub()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 if request body or signature is missing', async function () {
|
||||||
|
req.body = null;
|
||||||
|
await controller.handle(req, res);
|
||||||
|
expect(res.writeHead.calledWith(400)).to.be.true;
|
||||||
|
expect(res.end.called).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 if webhook signature is invalid', async function () {
|
||||||
|
deps.webhookManager.parseWebhook.throws(new Error('Invalid signature'));
|
||||||
|
await controller.handle(req, res);
|
||||||
|
expect(res.writeHead.calledWith(401)).to.be.true;
|
||||||
|
expect(res.end.called).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle customer.subscription.created event', async function () {
|
||||||
|
const event = {
|
||||||
|
type: 'customer.subscription.created',
|
||||||
|
data: {
|
||||||
|
object: {customer: 'cust_123', items: {data: [{price: {id: 'price_123'}}]}}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deps.webhookManager.parseWebhook.returns(event);
|
||||||
|
deps.memberRepository.get.resolves({id: 'member_123'});
|
||||||
|
|
||||||
|
await controller.handle(req, res);
|
||||||
|
|
||||||
|
expect(deps.memberRepository.get.calledWith({customer_id: 'cust_123'})).to.be.true;
|
||||||
|
expect(deps.memberRepository.linkSubscription.calledOnce).to.be.true;
|
||||||
|
expect(res.writeHead.calledWith(200)).to.be.true;
|
||||||
|
expect(res.end.called).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a donation in checkoutSessionEvent', async function () {
|
||||||
|
const session = {
|
||||||
|
mode: 'payment',
|
||||||
|
metadata: {
|
||||||
|
ghost_donation: true,
|
||||||
|
attribution_id: 'attr_123',
|
||||||
|
attribution_url: 'https://example.com',
|
||||||
|
attribution_type: 'referral',
|
||||||
|
referrer_source: 'google',
|
||||||
|
referrer_medium: 'cpc',
|
||||||
|
referrer_url: 'https://referrer.com'
|
||||||
|
},
|
||||||
|
amount_total: 5000,
|
||||||
|
currency: 'usd',
|
||||||
|
customer: 'cust_123',
|
||||||
|
customer_details: {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john@example.com'
|
||||||
|
},
|
||||||
|
custom_fields: [{
|
||||||
|
key: 'donation_message',
|
||||||
|
text: {
|
||||||
|
value: 'Thank you for the awesome newsletter!'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const member = {
|
||||||
|
id: 'member_123',
|
||||||
|
get: sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
|
member.get.withArgs('name').returns('John Doe');
|
||||||
|
member.get.withArgs('email').returns('john@example.com');
|
||||||
|
|
||||||
|
deps.memberRepository.get.resolves(member);
|
||||||
|
|
||||||
|
await controller.checkoutSessionEvent(session);
|
||||||
|
|
||||||
|
expect(deps.memberRepository.get.calledWith({customer_id: 'cust_123'})).to.be.true;
|
||||||
|
expect(deps.donationRepository.save.calledOnce).to.be.true;
|
||||||
|
expect(deps.staffServiceEmails.notifyDonationReceived.calledOnce).to.be.true;
|
||||||
|
|
||||||
|
const savedDonationEvent = deps.donationRepository.save.getCall(0).args[0];
|
||||||
|
expect(savedDonationEvent.amount).to.equal(5000);
|
||||||
|
expect(savedDonationEvent.currency).to.equal('usd');
|
||||||
|
expect(savedDonationEvent.name).to.equal('John Doe');
|
||||||
|
expect(savedDonationEvent.email).to.equal('john@example.com');
|
||||||
|
expect(savedDonationEvent.donationMessage).to.equal('Thank you for the awesome newsletter!');
|
||||||
|
expect(savedDonationEvent.attributionId).to.equal('attr_123');
|
||||||
|
expect(savedDonationEvent.attributionUrl).to.equal('https://example.com');
|
||||||
|
expect(savedDonationEvent.attributionType).to.equal('referral');
|
||||||
|
expect(savedDonationEvent.referrerSource).to.equal('google');
|
||||||
|
expect(savedDonationEvent.referrerMedium).to.equal('cpc');
|
||||||
|
expect(savedDonationEvent.referrerUrl).to.equal('https://referrer.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('donation message is null if string is empty', async function () {
|
||||||
|
const session = {
|
||||||
|
mode: 'payment',
|
||||||
|
metadata: {
|
||||||
|
ghost_donation: true,
|
||||||
|
attribution_id: 'attr_123',
|
||||||
|
attribution_url: 'https://example.com',
|
||||||
|
attribution_type: 'referral',
|
||||||
|
referrer_source: 'google',
|
||||||
|
referrer_medium: 'cpc',
|
||||||
|
referrer_url: 'https://referrer.com'
|
||||||
|
},
|
||||||
|
amount_total: 5000,
|
||||||
|
currency: 'usd',
|
||||||
|
customer: 'cust_123',
|
||||||
|
customer_details: {
|
||||||
|
name: 'JW',
|
||||||
|
email: 'jw@ily.co'
|
||||||
|
},
|
||||||
|
custom_fields: [{
|
||||||
|
key: 'donation_message',
|
||||||
|
text: {
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const member = {
|
||||||
|
id: 'member_123',
|
||||||
|
get: sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
|
member.get.withArgs('name').returns('JW');
|
||||||
|
member.get.withArgs('email').returns('jw@ily.co');
|
||||||
|
|
||||||
|
deps.memberRepository.get.resolves(member);
|
||||||
|
|
||||||
|
await controller.checkoutSessionEvent(session);
|
||||||
|
|
||||||
|
expect(deps.memberRepository.get.calledWith({customer_id: 'cust_123'})).to.be.true;
|
||||||
|
|
||||||
|
const savedDonationEvent = deps.donationRepository.save.getCall(0).args[0];
|
||||||
|
|
||||||
|
expect(savedDonationEvent.donationMessage).to.equal(null);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user