@@ -17,7 +17,11 @@ import {
1717 StripeLinkCreateParams ,
1818} from "api/functions/stripe.js" ;
1919import { getSecretValue } from "api/plugins/auth.js" ;
20- import { environmentConfig , genericConfig } from "common/config.js" ;
20+ import {
21+ environmentConfig ,
22+ genericConfig ,
23+ notificationRecipients ,
24+ } from "common/config.js" ;
2125import {
2226 BaseError ,
2327 DatabaseFetchError ,
@@ -333,7 +337,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
333337 } ) ;
334338 }
335339 switch ( event . type ) {
336- case "checkout.session.completed " :
340+ case "checkout.session.async_payment_failed " :
337341 if ( event . data . object . payment_link ) {
338342 const eventId = event . id ;
339343 const paymentAmount = event . data . object . amount_total ;
@@ -391,93 +395,263 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
391395 . formatToParts ( paymentAmount / 100 )
392396 . map ( ( val ) => val . value )
393397 . join ( "" ) ;
394- request . log . info (
395- `Registered payment of ${ withCurrency } by ${ name } (${ email } ) for payment link ${ paymentLinkId } invoice ID ${ unmarshalledEntry . invoiceId } ). Invoice was paid ${ paidInFull ? "in full." : "partially." } ` ,
396- ) ;
397- // Notify link owner of payment
398+
399+ // Notify link owner of failed payment
398400 let queueId ;
399- if ( unmarshalledEntry . userId . includes ( "@" ) ) {
401+ if ( event . data . object . payment_status === "unpaid" ) {
400402 request . log . info (
401- `Sending email to ${ unmarshalledEntry . userId } .. .` ,
403+ `Failed payment of ${ withCurrency } by ${ name } ( ${ email } ) for payment link ${ paymentLinkId } invoice ID ${ unmarshalledEntry . invoiceId } ) .` ,
402404 ) ;
403- const sqsPayload : SQSPayload < AvailableSQSFunctions . EmailNotifications > =
404- {
405- function : AvailableSQSFunctions . EmailNotifications ,
406- metadata : {
407- initiator : eventId ,
408- reqId : request . id ,
409- } ,
410- payload : {
411- to : [ unmarshalledEntry . userId ] ,
412- subject : `Payment Recieved for Invoice ${ unmarshalledEntry . invoiceId } ` ,
413- content : `ACM @ UIUC has received ${ paidInFull ? "full" : "partial" } payment for Invoice ${ unmarshalledEntry . invoiceId } (${ withCurrency } paid by ${ name } , ${ email } ).\n\nPlease contact Officer Board with any questions.` ,
414- callToActionButton : {
415- name : "View Your Stripe Links" ,
416- url : `${ fastify . environmentConfig . UserFacingUrl } /stripe` ,
405+ if ( unmarshalledEntry . userId . includes ( "@" ) ) {
406+ request . log . info (
407+ `Sending email to ${ unmarshalledEntry . userId } ...` ,
408+ ) ;
409+ const sqsPayload : SQSPayload < AvailableSQSFunctions . EmailNotifications > =
410+ {
411+ function : AvailableSQSFunctions . EmailNotifications ,
412+ metadata : {
413+ initiator : eventId ,
414+ reqId : request . id ,
417415 } ,
418- } ,
419- } ;
420- if ( ! fastify . sqsClient ) {
421- fastify . sqsClient = new SQSClient ( {
422- region : genericConfig . AwsRegion ,
423- } ) ;
416+ payload : {
417+ to : [ unmarshalledEntry . userId ] ,
418+ subject : `Payment Failed for Invoice ${ unmarshalledEntry . invoiceId } ` ,
419+ content : `
420+ A ${ paidInFull ? "full" : "partial" } payment for Invoice ${ unmarshalledEntry . invoiceId } (${ withCurrency } paid by ${ name } , ${ email } ) <b>has failed.</b>
421+
422+ Please ask the payee to try again, perhaps with a different payment method, or contact Officer Board.
423+ ` ,
424+ callToActionButton : {
425+ name : "View Your Stripe Links" ,
426+ url : `${ fastify . environmentConfig . UserFacingUrl } /stripe` ,
427+ } ,
428+ } ,
429+ } ;
430+ if ( ! fastify . sqsClient ) {
431+ fastify . sqsClient = new SQSClient ( {
432+ region : genericConfig . AwsRegion ,
433+ } ) ;
434+ }
435+ const result = await fastify . sqsClient . send (
436+ new SendMessageCommand ( {
437+ QueueUrl : fastify . environmentConfig . SqsQueueUrl ,
438+ MessageBody : JSON . stringify ( sqsPayload ) ,
439+ } ) ,
440+ ) ;
441+ queueId = result . MessageId || "" ;
424442 }
425- const result = await fastify . sqsClient . send (
426- new SendMessageCommand ( {
427- QueueUrl : fastify . environmentConfig . SqsQueueUrl ,
428- MessageBody : JSON . stringify ( sqsPayload ) ,
429- } ) ,
430- ) ;
431- queueId = result . MessageId || "" ;
432443 }
433- // If full payment is done, disable the link
434- if ( paidInFull ) {
435- request . log . debug ( "Paid in full, disabling link." ) ;
436- const logStatement = buildAuditLogTransactPut ( {
437- entry : {
438- module : Modules . STRIPE ,
439- actor : eventId ,
440- target : `Link ${ paymentLinkId } | Invoice ${ unmarshalledEntry . invoiceId } ` ,
441- message :
442- "Disabled Stripe payment link as payment was made in full." ,
444+
445+ return reply . status ( 200 ) . send ( {
446+ handled : true ,
447+ requestId : request . id ,
448+ queueId : queueId || "" ,
449+ } ) ;
450+ }
451+ return reply
452+ . code ( 200 )
453+ . send ( { handled : false , requestId : request . id } ) ;
454+ case "checkout.session.async_payment_succeeded" :
455+ case "checkout.session.completed" :
456+ if ( event . data . object . payment_link ) {
457+ const eventId = event . id ;
458+ const paymentAmount = event . data . object . amount_total ;
459+ const paymentCurrency = event . data . object . currency ;
460+ const { email, name } = event . data . object . customer_details || {
461+ email : null ,
462+ name : null ,
463+ } ;
464+ const paymentLinkId = event . data . object . payment_link . toString ( ) ;
465+ if ( ! paymentLinkId || ! paymentCurrency || ! paymentAmount ) {
466+ request . log . info ( "Missing required fields." ) ;
467+ return reply
468+ . code ( 200 )
469+ . send ( { handled : false , requestId : request . id } ) ;
470+ }
471+ const response = await fastify . dynamoClient . send (
472+ new QueryCommand ( {
473+ TableName : genericConfig . StripeLinksDynamoTableName ,
474+ IndexName : "LinkIdIndex" ,
475+ KeyConditionExpression : "linkId = :linkId" ,
476+ ExpressionAttributeValues : {
477+ ":linkId" : { S : paymentLinkId } ,
443478 } ,
479+ } ) ,
480+ ) ;
481+ if ( ! response ) {
482+ throw new DatabaseFetchError ( {
483+ message : "Could not check for payment link in table." ,
444484 } ) ;
445- const dynamoCommand = new TransactWriteItemsCommand ( {
446- TransactItems : [
447- ...( logStatement ? [ logStatement ] : [ ] ) ,
485+ }
486+ if ( ! response . Items || response . Items ?. length !== 1 ) {
487+ return reply . status ( 200 ) . send ( {
488+ handled : false ,
489+ requestId : request . id ,
490+ } ) ;
491+ }
492+ const unmarshalledEntry = unmarshall ( response . Items [ 0 ] ) as {
493+ userId : string ;
494+ invoiceId : string ;
495+ amount ?: number ;
496+ priceId ?: string ;
497+ productId ?: string ;
498+ } ;
499+ if ( ! unmarshalledEntry . userId || ! unmarshalledEntry . invoiceId ) {
500+ return reply . status ( 200 ) . send ( {
501+ handled : false ,
502+ requestId : request . id ,
503+ } ) ;
504+ }
505+ const paidInFull = paymentAmount === unmarshalledEntry . amount ;
506+ const withCurrency = new Intl . NumberFormat ( "en-US" , {
507+ style : "currency" ,
508+ currency : paymentCurrency . toUpperCase ( ) ,
509+ } )
510+ . formatToParts ( paymentAmount / 100 )
511+ . map ( ( val ) => val . value )
512+ . join ( "" ) ;
513+
514+ // Notify link owner of payment
515+ let queueId ;
516+ if ( event . data . object . payment_status === "unpaid" ) {
517+ request . log . info (
518+ `Pending payment of ${ withCurrency } by ${ name } (${ email } ) for payment link ${ paymentLinkId } invoice ID ${ unmarshalledEntry . invoiceId } ). Invoice was tentatively paid ${ paidInFull ? "in full." : "partially." } ` ,
519+ ) ;
520+ if ( unmarshalledEntry . userId . includes ( "@" ) ) {
521+ request . log . info (
522+ `Sending email to ${ unmarshalledEntry . userId } ...` ,
523+ ) ;
524+ const sqsPayload : SQSPayload < AvailableSQSFunctions . EmailNotifications > =
448525 {
449- Update : {
450- TableName : genericConfig . StripeLinksDynamoTableName ,
451- Key : {
452- userId : { S : unmarshalledEntry . userId } ,
453- linkId : { S : paymentLinkId } ,
526+ function : AvailableSQSFunctions . EmailNotifications ,
527+ metadata : {
528+ initiator : eventId ,
529+ reqId : request . id ,
530+ } ,
531+ payload : {
532+ to : [ unmarshalledEntry . userId ] ,
533+ subject : `Payment Pending for Invoice ${ unmarshalledEntry . invoiceId } ` ,
534+ content : `
535+ ACM @ UIUC has received intent of ${ paidInFull ? "full" : "partial" } payment for Invoice ${ unmarshalledEntry . invoiceId } (${ withCurrency } paid by ${ name } , ${ email } ).
536+
537+ The payee has used a payment method which does not settle funds immediately. Therefore, ACM @ UIUC is still waiting for funds to settle and <b>no services should be performed until the funds settle.</b>
538+
539+ Please contact Officer Board with any questions.
540+ ` ,
541+ callToActionButton : {
542+ name : "View Your Stripe Links" ,
543+ url : `${ fastify . environmentConfig . UserFacingUrl } /stripe` ,
454544 } ,
455- UpdateExpression : "SET active = :new_val" ,
456- ConditionExpression : "active = :old_val" ,
457- ExpressionAttributeValues : {
458- ":new_val" : { BOOL : false } ,
459- ":old_val" : { BOOL : true } ,
545+ } ,
546+ } ;
547+ if ( ! fastify . sqsClient ) {
548+ fastify . sqsClient = new SQSClient ( {
549+ region : genericConfig . AwsRegion ,
550+ } ) ;
551+ }
552+ const result = await fastify . sqsClient . send (
553+ new SendMessageCommand ( {
554+ QueueUrl : fastify . environmentConfig . SqsQueueUrl ,
555+ MessageBody : JSON . stringify ( sqsPayload ) ,
556+ } ) ,
557+ ) ;
558+ queueId = result . MessageId || "" ;
559+ }
560+ } else {
561+ request . log . info (
562+ `Registered payment of ${ withCurrency } by ${ name } (${ email } ) for payment link ${ paymentLinkId } invoice ID ${ unmarshalledEntry . invoiceId } ). Invoice was paid ${ paidInFull ? "in full." : "partially." } ` ,
563+ ) ;
564+ if ( unmarshalledEntry . userId . includes ( "@" ) ) {
565+ request . log . info (
566+ `Sending email to ${ unmarshalledEntry . userId } ...` ,
567+ ) ;
568+ const sqsPayload : SQSPayload < AvailableSQSFunctions . EmailNotifications > =
569+ {
570+ function : AvailableSQSFunctions . EmailNotifications ,
571+ metadata : {
572+ initiator : eventId ,
573+ reqId : request . id ,
574+ } ,
575+ payload : {
576+ to : [ unmarshalledEntry . userId ] ,
577+ cc : [
578+ notificationRecipients [ fastify . runEnvironment ]
579+ . Treasurer ,
580+ ] ,
581+ subject : `Payment Recieved for Invoice ${ unmarshalledEntry . invoiceId } ` ,
582+ content : `
583+ ACM @ UIUC has received ${ paidInFull ? "full" : "partial" } payment for Invoice ${ unmarshalledEntry . invoiceId } (${ withCurrency } paid by ${ name } , ${ email } ).
584+ ${ paidInFull ? "\nThis invoice should now be considered settled.\n" : "" }
585+ Please contact Officer Board with any questions.` ,
586+ callToActionButton : {
587+ name : "View Your Stripe Links" ,
588+ url : `${ fastify . environmentConfig . UserFacingUrl } /stripe` ,
460589 } ,
461590 } ,
462- } ,
463- ] ,
464- } ) ;
465- if ( unmarshalledEntry . productId ) {
466- request . log . debug (
467- `Deactivating Stripe product ${ unmarshalledEntry . productId } ` ,
591+ } ;
592+ if ( ! fastify . sqsClient ) {
593+ fastify . sqsClient = new SQSClient ( {
594+ region : genericConfig . AwsRegion ,
595+ } ) ;
596+ }
597+ const result = await fastify . sqsClient . send (
598+ new SendMessageCommand ( {
599+ QueueUrl : fastify . environmentConfig . SqsQueueUrl ,
600+ MessageBody : JSON . stringify ( sqsPayload ) ,
601+ } ) ,
468602 ) ;
469- await deactivateStripeProduct ( {
603+ queueId = result . MessageId || "" ;
604+ }
605+ // If full payment is done, disable the link
606+ if ( paidInFull ) {
607+ request . log . debug ( "Paid in full, disabling link." ) ;
608+ const logStatement = buildAuditLogTransactPut ( {
609+ entry : {
610+ module : Modules . STRIPE ,
611+ actor : eventId ,
612+ target : `Link ${ paymentLinkId } | Invoice ${ unmarshalledEntry . invoiceId } ` ,
613+ message :
614+ "Disabled Stripe payment link as payment was made in full." ,
615+ } ,
616+ } ) ;
617+ const dynamoCommand = new TransactWriteItemsCommand ( {
618+ TransactItems : [
619+ ...( logStatement ? [ logStatement ] : [ ] ) ,
620+ {
621+ Update : {
622+ TableName : genericConfig . StripeLinksDynamoTableName ,
623+ Key : {
624+ userId : { S : unmarshalledEntry . userId } ,
625+ linkId : { S : paymentLinkId } ,
626+ } ,
627+ UpdateExpression : "SET active = :new_val" ,
628+ ConditionExpression : "active = :old_val" ,
629+ ExpressionAttributeValues : {
630+ ":new_val" : { BOOL : false } ,
631+ ":old_val" : { BOOL : true } ,
632+ } ,
633+ } ,
634+ } ,
635+ ] ,
636+ } ) ;
637+ if ( unmarshalledEntry . productId ) {
638+ request . log . debug (
639+ `Deactivating Stripe product ${ unmarshalledEntry . productId } ` ,
640+ ) ;
641+ await deactivateStripeProduct ( {
642+ stripeApiKey : secretApiConfig . stripe_secret_key as string ,
643+ productId : unmarshalledEntry . productId ,
644+ } ) ;
645+ }
646+ request . log . debug ( `Deactivating Stripe link ${ paymentLinkId } ` ) ;
647+ await deactivateStripeLink ( {
470648 stripeApiKey : secretApiConfig . stripe_secret_key as string ,
471- productId : unmarshalledEntry . productId ,
649+ linkId : paymentLinkId ,
472650 } ) ;
651+ await fastify . dynamoClient . send ( dynamoCommand ) ;
473652 }
474- request . log . debug ( `Deactivating Stripe link ${ paymentLinkId } ` ) ;
475- await deactivateStripeLink ( {
476- stripeApiKey : secretApiConfig . stripe_secret_key as string ,
477- linkId : paymentLinkId ,
478- } ) ;
479- await fastify . dynamoClient . send ( dynamoCommand ) ;
480653 }
654+
481655 return reply . status ( 200 ) . send ( {
482656 handled : true ,
483657 requestId : request . id ,
0 commit comments