Skip to content

Commit

Permalink
Logged out donations (#46)
Browse files Browse the repository at this point in the history
* Added one-time form

* Allow anonymous access to add card and payment
  • Loading branch information
jzongker authored Apr 20, 2022
1 parent a692486 commit 91eaa69
Show file tree
Hide file tree
Showing 6 changed files with 461 additions and 438 deletions.
2 changes: 1 addition & 1 deletion src/apiBase
162 changes: 82 additions & 80 deletions src/controllers/DonateController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,94 +9,96 @@ import { Donation, FundDonation, DonationBatch, PaymentDetails, EventLog, Subscr
@controller("/donate")
export class DonateController extends GivingBaseController {

@httpPost("/log")
public async log(req: express.Request<{}, {}, { donation: Donation, fundData: {id: string, amount: number} }>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapperAnon(req, res, async () => {
const secretKey = await this.loadPrivateKey(req.body.donation.churchId);
const { donation, fundData } = req.body;
if (secretKey === "") return this.json({}, 401);
this.logDonation(donation, [fundData]);
});
}
@httpPost("/log")
public async log(req: express.Request<{}, {}, { donation: Donation, fundData: { id: string, amount: number } }>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapperAnon(req, res, async () => {
const secretKey = await this.loadPrivateKey(req.body.donation.churchId);
const { donation, fundData } = req.body;
if (secretKey === "") return this.json({}, 401);
this.logDonation(donation, [fundData]);
});
}

@httpPost("/webhook/:provider")
public async webhook(req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapperAnon(req, res, async () => {
const churchId = req.query.churchId.toString();
const gateways = await this.repositories.gateway.loadAll(churchId);
const secretKey = EncryptionHelper.decrypt(gateways[0].privateKey);
if (!gateways.length || secretKey === "") return this.json({}, 401);
@httpPost("/webhook/:provider")
public async webhook(req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapperAnon(req, res, async () => {
const churchId = req.query.churchId.toString();
const gateways = await this.repositories.gateway.loadAll(churchId);
const secretKey = EncryptionHelper.decrypt(gateways[0].privateKey);
if (!gateways.length || secretKey === "") return this.json({}, 401);

const sig = req.headers["stripe-signature"].toString();
const webhookKey = EncryptionHelper.decrypt(gateways[0].webhookKey);
const stripeEvent: Stripe.Event = await StripeHelper.verifySignature(secretKey, req, sig, webhookKey);
const eventData = stripeEvent.data.object as any; // https://github.com/stripe/stripe-node/issues/758
const subscriptionEvent = eventData.subscription || eventData.description?.toLowerCase().includes('subscription');
if (stripeEvent.type === 'charge.succeeded' && subscriptionEvent) return this.json({}, 200); // Ignore charge.succeeded from subscription events in place of invoice.paid for access to subscription id
const existingEvent = await this.repositories.eventLog.load(churchId, stripeEvent.id);
if (!existingEvent) await StripeHelper.logEvent(churchId, stripeEvent, eventData);
if (stripeEvent.type === 'charge.succeeded' || stripeEvent.type === 'invoice.paid') await StripeHelper.logDonation(secretKey, churchId, eventData);
});
}
const sig = req.headers["stripe-signature"].toString();
const webhookKey = EncryptionHelper.decrypt(gateways[0].webhookKey);
const stripeEvent: Stripe.Event = await StripeHelper.verifySignature(secretKey, req, sig, webhookKey);
const eventData = stripeEvent.data.object as any; // https://github.com/stripe/stripe-node/issues/758
const subscriptionEvent = eventData.subscription || eventData.description?.toLowerCase().includes('subscription');
if (stripeEvent.type === 'charge.succeeded' && subscriptionEvent) return this.json({}, 200); // Ignore charge.succeeded from subscription events in place of invoice.paid for access to subscription id
const existingEvent = await this.repositories.eventLog.load(churchId, stripeEvent.id);
if (!existingEvent) await StripeHelper.logEvent(churchId, stripeEvent, eventData);
if (stripeEvent.type === 'charge.succeeded' || stripeEvent.type === 'invoice.paid') await StripeHelper.logDonation(secretKey, churchId, eventData);
});
}

@httpPost("/charge")
public async charge(req: express.Request<any>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
const secretKey = await this.loadPrivateKey(au.churchId);
if (secretKey === "") return this.json({}, 401);
const donationData = req.body;
const fundDonations: FundDonation[] = donationData.funds;
const paymentData: PaymentDetails = { amount: donationData.amount, currency: 'usd', customer: donationData.customerId, metadata: { funds: JSON.stringify(fundDonations), notes: donationData.notes } };
if (donationData.type === 'card') {
paymentData.payment_method = donationData.id;
paymentData.confirm = true;
paymentData.off_session = true;
}
if (donationData.type === 'bank') paymentData.source = donationData.id;
return await StripeHelper.donate(secretKey, paymentData);
});
}
@httpPost("/charge")
public async charge(req: express.Request<any>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
const donationData = req.body;
const churchId = au.churchId || donationData.churchId;
const secretKey = await this.loadPrivateKey(churchId);
if (secretKey === "") return this.json({}, 401);

@httpPost("/subscribe")
public async subscribe(req: express.Request<any>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
const secretKey = await this.loadPrivateKey(au.churchId);
if (secretKey === "") return this.json({}, 401);
const fundDonations: FundDonation[] = donationData.funds;
const paymentData: PaymentDetails = { amount: donationData.amount, currency: 'usd', customer: donationData.customerId, metadata: { funds: JSON.stringify(fundDonations), notes: donationData.notes } };
if (donationData.type === 'card') {
paymentData.payment_method = donationData.id;
paymentData.confirm = true;
paymentData.off_session = true;
}
if (donationData.type === 'bank') paymentData.source = donationData.id;
return await StripeHelper.donate(secretKey, paymentData);
});
}

const { id, amount, customerId, type, billing_cycle_anchor, proration_behavior, interval, funds, person, notes } = req.body;
const paymentData: PaymentDetails = { payment_method_id: id, amount, currency: 'usd', customer: customerId, type, billing_cycle_anchor, proration_behavior, interval, metadata: { notes } };
const gateways = await this.repositories.gateway.loadAll(au.churchId);
paymentData.productId = gateways[0].productId;
@httpPost("/subscribe")
public async subscribe(req: express.Request<any>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
const secretKey = await this.loadPrivateKey(au.churchId);
if (secretKey === "") return this.json({}, 401);

const stripeSubscription = await StripeHelper.createSubscription(secretKey, paymentData);
const subscription: Subscription = { id: stripeSubscription.id, churchId: au.churchId, personId: person.id, customerId };
await this.repositories.subscription.save(subscription);
const { id, amount, customerId, type, billing_cycle_anchor, proration_behavior, interval, funds, person, notes } = req.body;
const paymentData: PaymentDetails = { payment_method_id: id, amount, currency: 'usd', customer: customerId, type, billing_cycle_anchor, proration_behavior, interval, metadata: { notes } };
const gateways = await this.repositories.gateway.loadAll(au.churchId);
paymentData.productId = gateways[0].productId;

const promises: Promise<SubscriptionFund>[] = [];
funds.forEach((fund: FundDonation) => {
const subscriptionFund: SubscriptionFund = { churchId: au.churchId, subscriptionId: subscription.id, fundId: fund.id, amount: fund.amount };
promises.push(this.repositories.subscriptionFund.save(subscriptionFund));
});
await Promise.all(promises);
return stripeSubscription;
});
}
const stripeSubscription = await StripeHelper.createSubscription(secretKey, paymentData);
const subscription: Subscription = { id: stripeSubscription.id, churchId: au.churchId, personId: person.id, customerId };
await this.repositories.subscription.save(subscription);

private logDonation = async (donationData: Donation, fundData: FundDonation[]) => {
const batch: DonationBatch = await this.repositories.donationBatch.getOrCreateCurrent(donationData.churchId);
donationData.batchId = batch.id;
const donation = await this.repositories.donation.save(donationData);
const promises: Promise<FundDonation>[] = [];
fundData.forEach((fund: FundDonation) => {
const fundDonation: FundDonation = { churchId: donation.churchId, amount: fund.amount, donationId: donation.id, fundId: fund.id };
promises.push(this.repositories.fundDonation.save(fundDonation));
});
return await Promise.all(promises);
}
const promises: Promise<SubscriptionFund>[] = [];
funds.forEach((fund: FundDonation) => {
const subscriptionFund: SubscriptionFund = { churchId: au.churchId, subscriptionId: subscription.id, fundId: fund.id, amount: fund.amount };
promises.push(this.repositories.subscriptionFund.save(subscriptionFund));
});
await Promise.all(promises);
return stripeSubscription;
});
}

private loadPrivateKey = async (churchId: string) => {
const gateways = await this.repositories.gateway.loadAll(churchId);
return (gateways.length === 0) ? "" : EncryptionHelper.decrypt(gateways[0].privateKey);
}
private logDonation = async (donationData: Donation, fundData: FundDonation[]) => {
const batch: DonationBatch = await this.repositories.donationBatch.getOrCreateCurrent(donationData.churchId);
donationData.batchId = batch.id;
const donation = await this.repositories.donation.save(donationData);
const promises: Promise<FundDonation>[] = [];
fundData.forEach((fund: FundDonation) => {
const fundDonation: FundDonation = { churchId: donation.churchId, amount: fund.amount, donationId: donation.id, fundId: fund.id };
promises.push(this.repositories.fundDonation.save(fundDonation));
});
return await Promise.all(promises);
}

private loadPrivateKey = async (churchId: string) => {
const gateways = await this.repositories.gateway.loadAll(churchId);
return (gateways.length === 0) ? "" : EncryptionHelper.decrypt(gateways[0].privateKey);
}

}
71 changes: 39 additions & 32 deletions src/controllers/FundController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,47 @@ import { Permissions } from '../helpers/Permissions'
@controller("/funds")
export class FundController extends GivingBaseController {

@httpGet("/:id")
public async get(@requestParam("id") id: string, req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
if (!au.checkAccess(Permissions.donations.viewSummary)) return this.json({}, 401);
else return this.repositories.fund.convertToModel(au.churchId, await this.repositories.fund.load(au.churchId, id));
});
}
@httpGet("/:id")
public async get(@requestParam("id") id: string, req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
if (!au.checkAccess(Permissions.donations.viewSummary)) return this.json({}, 401);
else return this.repositories.fund.convertToModel(au.churchId, await this.repositories.fund.load(au.churchId, id));
});
}

@httpGet("/")
public async getAll(req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
return this.repositories.fund.convertAllToModel(au.churchId, await this.repositories.fund.loadAll(au.churchId));
});
}
@httpGet("/churchId/:churchId")
public async getForChurch(@requestParam("churchId") churchId: string, req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapperAnon(req, res, async () => {
return this.repositories.fund.convertAllToModel(churchId, await this.repositories.fund.loadAll(churchId));
});
}

@httpPost("/")
public async save(req: express.Request<{}, {}, Fund[]>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
if (!au.checkAccess(Permissions.donations.edit)) return this.json({}, 401);
else {
const promises: Promise<Fund>[] = [];
req.body.forEach(fund => { fund.churchId = au.churchId; promises.push(this.repositories.fund.save(fund)); });
const result = await Promise.all(promises);
return this.repositories.fund.convertAllToModel(au.churchId, result);
}
});
}
@httpGet("/")
public async getAll(req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
return this.repositories.fund.convertAllToModel(au.churchId, await this.repositories.fund.loadAll(au.churchId));
});
}

@httpDelete("/:id")
public async delete(@requestParam("id") id: string, req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
if (!au.checkAccess(Permissions.donations.edit)) return this.json({}, 401);
else await this.repositories.fund.delete(au.churchId, id);
});
}
@httpPost("/")
public async save(req: express.Request<{}, {}, Fund[]>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
if (!au.checkAccess(Permissions.donations.edit)) return this.json({}, 401);
else {
const promises: Promise<Fund>[] = [];
req.body.forEach(fund => { fund.churchId = au.churchId; promises.push(this.repositories.fund.save(fund)); });
const result = await Promise.all(promises);
return this.repositories.fund.convertAllToModel(au.churchId, result);
}
});
}

@httpDelete("/:id")
public async delete(@requestParam("id") id: string, req: express.Request<{}, {}, null>, res: express.Response): Promise<interfaces.IHttpActionResult> {
return this.actionWrapper(req, res, async (au) => {
if (!au.checkAccess(Permissions.donations.edit)) return this.json({}, 401);
else await this.repositories.fund.delete(au.churchId, id);
});
}

}
Loading

0 comments on commit 91eaa69

Please sign in to comment.