11/**
22 * The level-triggered usage threshold email against real claim state in a disposable PostgreSQL
3- * schema. Only delivery is stubbed: the mailer is the external boundary, and a sent email is the
4- * outcome under test .
3+ * schema. Only delivery is stubbed: the mailer is the external boundary, so the outcomes under
4+ * test are the messages handed to it and the claim state left in the database .
55 */
66import type { db } from '@sim/db'
77import * as schema from '@sim/db/schema'
88import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure'
99import { emailMailerMock , emailMailerMockFns } from '@sim/testing/mocks/email-mailer.mock'
10- import { emailTemplatesMock } from '@sim/testing/mocks/email-templates.mock'
10+ import { emailTemplatesMock , emailTemplatesMockFns } from '@sim/testing/mocks/email-templates.mock'
1111import { envFlagsMock , resetEnvFlagsMock , setEnvFlags } from '@sim/testing/mocks/env-flags.mock'
1212import { generateId } from '@sim/utils/id'
1313import { drizzle } from 'drizzle-orm/postgres-js'
@@ -59,6 +59,40 @@ function notify(
5959 } )
6060}
6161
62+ /** Every message handed to the mailer so far, as who received which email. */
63+ function delivered ( ) : { to : string ; subject : string } [ ] {
64+ return mockSendEmail . mock . calls . map ( ( [ message ] ) => ( { to : message . to , subject : message . subject } ) )
65+ }
66+
67+ function warning ( ) : { to : string ; subject : string } {
68+ return {
69+ to : `${ adminId } @example.com` ,
70+ subject : emailTemplatesMockFns . mockGetEmailSubject ( 'usage-threshold' ) ,
71+ }
72+ }
73+
74+ function reached ( ) : { to : string ; subject : string } {
75+ return {
76+ to : `${ adminId } @example.com` ,
77+ subject : emailTemplatesMockFns . mockGetLimitEmailSubject ( 'credits' , 'reached' ) ,
78+ }
79+ }
80+
81+ /** The organization's persisted threshold claims. */
82+ async function claims ( ) : Promise < Record < string , number > > {
83+ const [ row ] = await connection < { limit_notifications : Record < string , number > | null } [ ] > `
84+ SELECT limit_notifications FROM organization WHERE id = ${ organizationId } `
85+ return row . limit_notifications ?? { }
86+ }
87+
88+ function claimOf ( threshold : 80 | 100 , periodStart = SEPTEMBER , limitCents = 10_000 ) {
89+ return {
90+ credits : threshold ,
91+ creditsPeriod : Math . floor ( periodStart . getTime ( ) / 86_400_000 ) ,
92+ creditsLimit : limitCents ,
93+ }
94+ }
95+
6296async function setNotificationsEnabled ( enabled : boolean ) : Promise < void > {
6397 await connection `INSERT INTO settings VALUES (${ adminId } , ${ adminId } , ${ enabled } )
6498 ON CONFLICT (id) DO UPDATE SET billing_usage_notifications_enabled = ${ enabled } `
@@ -97,7 +131,8 @@ describe('usage threshold email', () => {
97131 await Promise . all ( [ notify ( 85 ) , notify ( 85 ) , notify ( 86 ) ] )
98132 await notify ( 90 )
99133
100- expect ( mockSendEmail ) . toHaveBeenCalledTimes ( 1 )
134+ expect ( delivered ( ) ) . toEqual ( [ warning ( ) ] )
135+ expect ( await claims ( ) ) . toEqual ( claimOf ( 80 ) )
101136 } )
102137
103138 it ( 'still sends the reached email after the warning, but never the warning after it' , async ( ) => {
@@ -106,7 +141,8 @@ describe('usage threshold email', () => {
106141 await notify ( 100 )
107142 await notify ( 85 )
108143
109- expect ( mockSendEmail ) . toHaveBeenCalledTimes ( 2 )
144+ expect ( delivered ( ) ) . toEqual ( [ warning ( ) , reached ( ) ] )
145+ expect ( await claims ( ) ) . toEqual ( claimOf ( 100 ) )
110146 } )
111147
112148 it ( 're-arms both thresholds whenever the billing period changes, even to an earlier one' , async ( ) => {
@@ -115,31 +151,36 @@ describe('usage threshold email', () => {
115151 await notify ( 100 , { periodStart : OCTOBER } )
116152 await notify ( 85 )
117153
118- expect ( mockSendEmail ) . toHaveBeenCalledTimes ( 4 )
154+ expect ( delivered ( ) ) . toEqual ( [ reached ( ) , warning ( ) , reached ( ) , warning ( ) ] )
155+ expect ( await claims ( ) ) . toEqual ( claimOf ( 80 ) )
119156 } )
120157
121158 it ( 'warns again at a raised limit after the old one was reached' , async ( ) => {
122159 await notify ( 100 )
123160 await notify ( 100 , { limit : 125 } )
124161 await notify ( 110 , { limit : 125 } )
125162
126- expect ( mockSendEmail ) . toHaveBeenCalledTimes ( 2 )
163+ expect ( delivered ( ) ) . toEqual ( [ reached ( ) , warning ( ) ] )
164+ expect ( await claims ( ) ) . toEqual ( claimOf ( 80 , SEPTEMBER , 12_500 ) )
127165 } )
128166
129167 it ( 'keeps the claim for a later completion when nobody can be notified' , async ( ) => {
130168 await setNotificationsEnabled ( false )
131169 await notify ( 90 )
170+ expect ( delivered ( ) ) . toEqual ( [ ] )
171+ expect ( await claims ( ) ) . toEqual ( { } )
172+
132173 await setNotificationsEnabled ( true )
133174 await notify ( 90 )
134-
135- expect ( mockSendEmail ) . toHaveBeenCalledTimes ( 1 )
175+ expect ( delivered ( ) ) . toEqual ( [ warning ( ) ] )
136176 } )
137177
138178 it ( 'keeps the claim when a completion recorded no cost' , async ( ) => {
139179 await notify ( 90 , { costDelta : 0 } )
140- expect ( mockSendEmail ) . not . toHaveBeenCalled ( )
180+ expect ( delivered ( ) ) . toEqual ( [ ] )
181+ expect ( await claims ( ) ) . toEqual ( { } )
141182
142183 await notify ( 90 )
143- expect ( mockSendEmail ) . toHaveBeenCalledTimes ( 1 )
184+ expect ( delivered ( ) ) . toEqual ( [ warning ( ) ] )
144185 } )
145186} )
0 commit comments