@@ -354,6 +354,18 @@ it("refunds partial amounts for non-test mode one-time purchases", async () => {
354354 accessType : "client" ,
355355 } ) ;
356356 expect ( productsAfterRes . body . items ) . toHaveLength ( 0 ) ;
357+
358+ const secondRefundAttempt = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
359+ accessType : "admin" ,
360+ method : "POST" ,
361+ body : {
362+ type : "one-time-purchase" ,
363+ id : purchaseTransaction . id ,
364+ amount_usd : "1250" ,
365+ refund_entries : [ { entry_index : 0 , quantity : 1 } ] ,
366+ } ,
367+ } ) ;
368+ expect ( secondRefundAttempt . body . code ) . toBe ( "ONE_TIME_PURCHASE_ALREADY_REFUNDED" ) ;
357369} ) ;
358370
359371it ( "refunds selected quantities for non-test mode one-time purchases" , async ( ) => {
@@ -388,3 +400,350 @@ it("refunds selected quantities for non-test mode one-time purchases", async ()
388400 } ) ;
389401 expect ( productsAfterRes . body . items ) . toHaveLength ( 0 ) ;
390402} ) ;
403+
404+ it ( "returns SCHEMA_ERROR when amount_usd is negative" , async ( ) => {
405+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
406+
407+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
408+ accessType : "admin" ,
409+ method : "POST" ,
410+ body : {
411+ type : "one-time-purchase" ,
412+ id : purchaseTransaction . id ,
413+ amount_usd : "-1" ,
414+ refund_entries : [ { entry_index : 0 , quantity : 1 } ] ,
415+ } ,
416+ } ) ;
417+ expect ( refundRes ) . toMatchInlineSnapshot ( `
418+ NiceResponse {
419+ "status": 400,
420+ "body": {
421+ "code": "SCHEMA_ERROR",
422+ "details": {
423+ "message": deindent\`
424+ Request validation failed on POST /api/latest/internal/payments/transactions/refund:
425+ - Money amount must be in the format of <number> or <number>.<number>
426+ \`,
427+ },
428+ "error": deindent\`
429+ Request validation failed on POST /api/latest/internal/payments/transactions/refund:
430+ - Money amount must be in the format of <number> or <number>.<number>
431+ \`,
432+ },
433+ "headers": Headers {
434+ "x-stack-known-error": "SCHEMA_ERROR",
435+ <some fields may have been hidden>,
436+ },
437+ }
438+ ` ) ;
439+ } ) ;
440+
441+ it ( "allows amount_usd of zero" , async ( ) => {
442+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
443+
444+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
445+ accessType : "admin" ,
446+ method : "POST" ,
447+ body : {
448+ type : "one-time-purchase" ,
449+ id : purchaseTransaction . id ,
450+ amount_usd : "0" ,
451+ refund_entries : [ { entry_index : 0 , quantity : 1 } ] ,
452+ } ,
453+ } ) ;
454+ expect ( refundRes ) . toMatchInlineSnapshot ( `
455+ NiceResponse {
456+ "status": 200,
457+ "body": { "success": true },
458+ "headers": Headers { <some fields may have been hidden> },
459+ }
460+ ` ) ;
461+ } ) ;
462+
463+ it ( "allows empty refund_entries (money-only refund)" , async ( ) => {
464+ const { userId, purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
465+
466+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
467+ accessType : "admin" ,
468+ method : "POST" ,
469+ body : {
470+ type : "one-time-purchase" ,
471+ id : purchaseTransaction . id ,
472+ amount_usd : "5000" ,
473+ refund_entries : [ ] ,
474+ } ,
475+ } ) ;
476+ expect ( refundRes . status ) . toBe ( 200 ) ;
477+ expect ( refundRes . body ) . toEqual ( { success : true } ) ;
478+
479+ const transactionsAfterRefund = await niceBackendFetch ( "/api/latest/internal/payments/transactions" , {
480+ accessType : "admin" ,
481+ } ) ;
482+ const refundedTransaction = transactionsAfterRefund . body . transactions . find ( ( tx : any ) => tx . id === purchaseTransaction . id ) ;
483+ expect ( refundedTransaction ?. adjusted_by ) . toEqual ( [
484+ {
485+ entry_index : 0 ,
486+ transaction_id : expect . stringContaining ( `${ purchaseTransaction . id } :refund` ) ,
487+ } ,
488+ ] ) ;
489+
490+ const productsAfterRes = await niceBackendFetch ( `/api/v1/payments/products/user/${ userId } ` , {
491+ accessType : "client" ,
492+ } ) ;
493+ expect ( productsAfterRes . body . items ) . toHaveLength ( 0 ) ;
494+ } ) ;
495+
496+ it ( "returns SCHEMA_ERROR when refund_entries contains bad entry_index" , async ( ) => {
497+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
498+
499+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
500+ accessType : "admin" ,
501+ method : "POST" ,
502+ body : {
503+ type : "one-time-purchase" ,
504+ id : purchaseTransaction . id ,
505+ amount_usd : "5000" ,
506+ refund_entries : [ { entry_index : 999 , quantity : 1 } ] ,
507+ } ,
508+ } ) ;
509+ expect ( refundRes ) . toMatchInlineSnapshot ( `
510+ NiceResponse {
511+ "status": 400,
512+ "body": {
513+ "code": "SCHEMA_ERROR",
514+ "details": { "message": "Refund entry index is invalid." },
515+ "error": "Refund entry index is invalid.",
516+ },
517+ "headers": Headers {
518+ "x-stack-known-error": "SCHEMA_ERROR",
519+ <some fields may have been hidden>,
520+ },
521+ }
522+ ` ) ;
523+ } ) ;
524+
525+ it ( "returns SCHEMA_ERROR when refund_entries contains negative quantity" , async ( ) => {
526+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
527+
528+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
529+ accessType : "admin" ,
530+ method : "POST" ,
531+ body : {
532+ type : "one-time-purchase" ,
533+ id : purchaseTransaction . id ,
534+ amount_usd : "5000" ,
535+ refund_entries : [ { entry_index : 0 , quantity : - 1 } ] ,
536+ } ,
537+ } ) ;
538+ expect ( refundRes ) . toMatchInlineSnapshot ( `
539+ NiceResponse {
540+ "status": 400,
541+ "body": {
542+ "code": "SCHEMA_ERROR",
543+ "details": { "message": "Refund quantity cannot be negative." },
544+ "error": "Refund quantity cannot be negative.",
545+ },
546+ "headers": Headers {
547+ "x-stack-known-error": "SCHEMA_ERROR",
548+ <some fields may have been hidden>,
549+ },
550+ }
551+ ` ) ;
552+ } ) ;
553+
554+ it ( "allows refund_entries with zero quantity" , async ( ) => {
555+ const { userId, purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
556+
557+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
558+ accessType : "admin" ,
559+ method : "POST" ,
560+ body : {
561+ type : "one-time-purchase" ,
562+ id : purchaseTransaction . id ,
563+ amount_usd : "5000" ,
564+ refund_entries : [ { entry_index : 0 , quantity : 0 } ] ,
565+ } ,
566+ } ) ;
567+ expect ( refundRes . status ) . toBe ( 200 ) ;
568+ expect ( refundRes . body ) . toEqual ( { success : true } ) ;
569+
570+ const transactionsAfterRefund = await niceBackendFetch ( "/api/latest/internal/payments/transactions" , {
571+ accessType : "admin" ,
572+ } ) ;
573+ const refundedTransaction = transactionsAfterRefund . body . transactions . find ( ( tx : any ) => tx . id === purchaseTransaction . id ) ;
574+ expect ( refundedTransaction ?. adjusted_by ) . toEqual ( [
575+ {
576+ entry_index : 0 ,
577+ transaction_id : expect . stringContaining ( `${ purchaseTransaction . id } :refund` ) ,
578+ } ,
579+ ] ) ;
580+
581+ const productsAfterRes = await niceBackendFetch ( `/api/v1/payments/products/user/${ userId } ` , {
582+ accessType : "client" ,
583+ } ) ;
584+ expect ( productsAfterRes . body . items ) . toHaveLength ( 0 ) ;
585+ } ) ;
586+
587+ it ( "returns SCHEMA_ERROR when refund_entries contains quantity past limit" , async ( ) => {
588+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( { quantity : 1 } ) ;
589+
590+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
591+ accessType : "admin" ,
592+ method : "POST" ,
593+ body : {
594+ type : "one-time-purchase" ,
595+ id : purchaseTransaction . id ,
596+ amount_usd : "5000" ,
597+ refund_entries : [ { entry_index : 0 , quantity : 2 } ] ,
598+ } ,
599+ } ) ;
600+ expect ( refundRes ) . toMatchInlineSnapshot ( `
601+ NiceResponse {
602+ "status": 400,
603+ "body": {
604+ "code": "SCHEMA_ERROR",
605+ "details": { "message": "Refund quantity cannot exceed purchased quantity." },
606+ "error": "Refund quantity cannot exceed purchased quantity.",
607+ },
608+ "headers": Headers {
609+ "x-stack-known-error": "SCHEMA_ERROR",
610+ <some fields may have been hidden>,
611+ },
612+ }
613+ ` ) ;
614+ } ) ;
615+
616+ it ( "returns SCHEMA_ERROR when amount_usd exceeds charged amount" , async ( ) => {
617+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
618+
619+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
620+ accessType : "admin" ,
621+ method : "POST" ,
622+ body : {
623+ type : "one-time-purchase" ,
624+ id : purchaseTransaction . id ,
625+ amount_usd : "5001" ,
626+ refund_entries : [ { entry_index : 0 , quantity : 1 } ] ,
627+ } ,
628+ } ) ;
629+ expect ( refundRes ) . toMatchInlineSnapshot ( `
630+ NiceResponse {
631+ "status": 400,
632+ "body": {
633+ "code": "SCHEMA_ERROR",
634+ "details": { "message": "Refund amount cannot exceed the charged amount." },
635+ "error": "Refund amount cannot exceed the charged amount.",
636+ },
637+ "headers": Headers {
638+ "x-stack-known-error": "SCHEMA_ERROR",
639+ <some fields may have been hidden>,
640+ },
641+ }
642+ ` ) ;
643+ } ) ;
644+
645+ it ( "returns SCHEMA_ERROR when refund_entries contains negative entry_index" , async ( ) => {
646+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
647+
648+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
649+ accessType : "admin" ,
650+ method : "POST" ,
651+ body : {
652+ type : "one-time-purchase" ,
653+ id : purchaseTransaction . id ,
654+ amount_usd : "5000" ,
655+ refund_entries : [ { entry_index : - 1 , quantity : 1 } ] ,
656+ } ,
657+ } ) ;
658+ expect ( refundRes ) . toMatchInlineSnapshot ( `
659+ NiceResponse {
660+ "status": 400,
661+ "body": {
662+ "code": "SCHEMA_ERROR",
663+ "details": { "message": "Refund entry index is invalid." },
664+ "error": "Refund entry index is invalid.",
665+ },
666+ "headers": Headers {
667+ "x-stack-known-error": "SCHEMA_ERROR",
668+ <some fields may have been hidden>,
669+ },
670+ }
671+ ` ) ;
672+ } ) ;
673+
674+ it ( "returns SCHEMA_ERROR when refund_entries quantity is not an integer" , async ( ) => {
675+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
676+
677+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
678+ accessType : "admin" ,
679+ method : "POST" ,
680+ body : {
681+ type : "one-time-purchase" ,
682+ id : purchaseTransaction . id ,
683+ amount_usd : "5000" ,
684+ refund_entries : [ { entry_index : 0 , quantity : 1.5 } ] ,
685+ } ,
686+ } ) ;
687+ expect ( refundRes . body . code ) . toBe ( "SCHEMA_ERROR" ) ;
688+ } ) ;
689+
690+ it ( "returns SCHEMA_ERROR when refund_entries references non-product_grant entries" , async ( ) => {
691+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( ) ;
692+
693+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
694+ accessType : "admin" ,
695+ method : "POST" ,
696+ body : {
697+ type : "one-time-purchase" ,
698+ id : purchaseTransaction . id ,
699+ amount_usd : "5000" ,
700+ refund_entries : [ { entry_index : 1 , quantity : 1 } ] ,
701+ } ,
702+ } ) ;
703+ expect ( refundRes ) . toMatchInlineSnapshot ( `
704+ NiceResponse {
705+ "status": 400,
706+ "body": {
707+ "code": "SCHEMA_ERROR",
708+ "details": { "message": "Refund entries must reference product grant entries." },
709+ "error": "Refund entries must reference product grant entries.",
710+ },
711+ "headers": Headers {
712+ "x-stack-known-error": "SCHEMA_ERROR",
713+ <some fields may have been hidden>,
714+ },
715+ }
716+ ` ) ;
717+ } ) ;
718+
719+ it ( "returns SCHEMA_ERROR when refund_entries contains duplicate entry indexes" , async ( ) => {
720+ const { purchaseTransaction } = await createLiveModeOneTimePurchaseTransaction ( { quantity : 2 } ) ;
721+
722+ const refundRes = await niceBackendFetch ( "/api/latest/internal/payments/transactions/refund" , {
723+ accessType : "admin" ,
724+ method : "POST" ,
725+ body : {
726+ type : "one-time-purchase" ,
727+ id : purchaseTransaction . id ,
728+ amount_usd : "5000" ,
729+ refund_entries : [
730+ { entry_index : 0 , quantity : 1 } ,
731+ { entry_index : 0 , quantity : 1 } ,
732+ ] ,
733+ } ,
734+ } ) ;
735+ expect ( refundRes ) . toMatchInlineSnapshot ( `
736+ NiceResponse {
737+ "status": 400,
738+ "body": {
739+ "code": "SCHEMA_ERROR",
740+ "details": { "message": "Refund entries cannot contain duplicate entry indexes." },
741+ "error": "Refund entries cannot contain duplicate entry indexes.",
742+ },
743+ "headers": Headers {
744+ "x-stack-known-error": "SCHEMA_ERROR",
745+ <some fields may have been hidden>,
746+ },
747+ }
748+ ` ) ;
749+ } ) ;
0 commit comments