1use super::*;
2
3#[derive(Clone, Copy)]
4enum HostChargePreSubmissionStage<'a> {
5 Unreserved,
6 Reserved {
7 targets: &'a dyn HostChargeTargetStore,
8 reservation: &'a HostChargeReservation,
9 resolution: HostChargeBeforeSubmissionResolution,
10 },
11}
12
13enum HostChargePreSubmissionOutcome<T = ()> {
14 Ready(T),
15 Resolved(Box<HostChargePaymentResult>),
16}
17
18#[derive(Clone, Copy)]
19struct HostChargeCooldownSurface {
20 scope: GatewayMutationCooldownScope,
21 code: PaymentResolutionCode,
22}
23
24impl<T> HostChargePreSubmissionOutcome<T> {
25 fn resolved(payment: HostChargePaymentResult) -> Self {
26 Self::Resolved(Box::new(payment))
27 }
28}
29
30fn host_charge_payment_result(
31 attempt: PaymentAttempt,
32) -> Result<HostChargePaymentResult, SubscriptionBillingServiceError> {
33 HostChargePaymentResult::new(attempt)
34 .map_err(|_| SubscriptionBillingServiceError::InvalidState(INVALID_SERVICE_STATE))
35}
36
37impl SubscriptionBillingService {
38 pub async fn charge_host_target(
43 &self,
44 command: ChargeHostTarget,
45 ) -> Result<HostChargePaymentResult, SubscriptionBillingServiceError> {
46 let targets = self
47 .host_charge_targets
48 .as_deref()
49 .ok_or(SubscriptionBillingServiceError::HostChargeUnavailable)?;
50 let (snapshot, prepared_reservation) = match self
51 .preflight_host_charge(targets, &command)
52 .await?
53 {
54 HostChargePreflightOutcome::Continue(snapshot) => (snapshot, None),
55 HostChargePreflightOutcome::Replay(attempt) if attempt_is_prepared(&attempt) => {
56 let reservation = HostChargeReservation::from_attempt(&attempt).map_err(|_| {
57 SubscriptionBillingServiceError::InvalidState(INVALID_SERVICE_STATE)
58 })?;
59 (reservation.snapshot(), Some(reservation))
60 }
61 HostChargePreflightOutcome::Replay(attempt) => {
62 return host_charge_payment_result(*attempt);
63 }
64 HostChargePreflightOutcome::IdempotencyConflict => {
65 return Err(SubscriptionBillingServiceError::IdempotencyConflict);
66 }
67 HostChargePreflightOutcome::Rejected { reason } => {
68 return Err(SubscriptionBillingServiceError::HostChargeReservationRejected(reason));
69 }
70 };
71 if prepared_reservation.as_ref().is_some_and(|reservation| {
72 reservation.identity().required_gateway_account_mode()
73 != self.required_gateway_account_mode
74 }) {
75 return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
76 }
77
78 let account = self
79 .gateway_account(
80 command.billing_scope_id(),
81 command.gateway_configuration_id(),
82 )
83 .await?;
84 if prepared_reservation.as_ref().is_some_and(|reservation| {
85 reservation.identity().gateway_account_id() != account.account_id
86 }) {
87 return Err(SubscriptionBillingServiceError::InvalidState(
88 INVALID_SERVICE_STATE,
89 ));
90 }
91 let cooldown_stage = prepared_reservation.as_ref().map_or(
92 HostChargePreSubmissionStage::Unreserved,
93 |reservation| HostChargePreSubmissionStage::Reserved {
94 targets,
95 reservation,
96 resolution: HostChargeBeforeSubmissionResolution::prepared(),
97 },
98 );
99 match self.host_charge_cooldown(&account, cooldown_stage).await? {
100 HostChargePreSubmissionOutcome::Ready(()) => {}
101 HostChargePreSubmissionOutcome::Resolved(payment) => return Ok(*payment),
102 }
103 let gateway = self
104 .resolver
105 .resolve(
106 command.billing_scope_id(),
107 account.account_id,
108 command.gateway_configuration_id(),
109 account.provider_key.clone(),
110 )
111 .await?;
112 if gateway.billing_scope_id() != command.billing_scope_id()
113 || gateway.gateway_account_id() != account.account_id
114 || gateway.gateway_configuration_id() != command.gateway_configuration_id()
115 || gateway.provider_key() != &account.provider_key
116 {
117 return Err(SubscriptionBillingServiceError::ResolvedGatewayIdentityMismatch);
118 }
119 if prepared_reservation.as_ref().is_some_and(|reservation| {
120 reservation.identity().gateway_account_id() != gateway.gateway_account_id()
121 || reservation.request().gateway_order_id()
122 != &gateway.mutation_reference_factory().for_attempt(
123 PaymentAttemptKind::HostCharge,
124 reservation.identity().attempt_id(),
125 )
126 }) {
127 return Err(SubscriptionBillingServiceError::InvalidState(
128 INVALID_SERVICE_STATE,
129 ));
130 }
131 let verified_gateway = if let Some(reservation) = prepared_reservation.as_ref() {
132 match self
133 .host_charge_prepared_gateway_readiness(&gateway, targets, reservation)
134 .await?
135 {
136 HostChargePreSubmissionOutcome::Ready(verified_gateway) => verified_gateway,
137 HostChargePreSubmissionOutcome::Resolved(payment) => return Ok(*payment),
138 }
139 } else {
140 self.host_charge_unreserved_gateway_readiness(&account, &gateway)
141 .await?
142 };
143 if prepared_reservation.is_none() {
144 self.admit_subscriber_mutation(
145 command.billing_scope_id(),
146 command.subscriber_id(),
147 EndUserMutationOperation::HostCharge,
148 )
149 .await?;
150 }
151
152 let candidate_id = prepared_reservation.as_ref().map_or_else(
153 || PaymentAttemptId::new(uuid::Uuid::now_v7()),
154 |reservation| reservation.identity().attempt_id(),
155 );
156 let mut reservation = match prepared_reservation {
157 Some(reservation) => reservation,
158 None => HostChargeReservation::from_command(
159 &command,
160 snapshot,
161 &gateway,
162 candidate_id,
163 self.required_gateway_account_mode,
164 )
165 .map_err(|_| SubscriptionBillingServiceError::InvalidState(INVALID_SERVICE_STATE))?,
166 };
167 let attempt = match self.reserve_host_charge(targets, &reservation).await? {
168 HostChargeReservationOutcome::Reserved(attempt)
169 | HostChargeReservationOutcome::Replay(attempt)
170 if attempt_is_prepared(&attempt) =>
171 {
172 attempt
173 }
174 HostChargeReservationOutcome::Replay(attempt) => {
175 return host_charge_payment_result(attempt);
176 }
177 HostChargeReservationOutcome::IdempotencyConflict => {
178 return Err(SubscriptionBillingServiceError::IdempotencyConflict);
179 }
180 HostChargeReservationOutcome::GatewayAccountModeChanged => {
181 return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
182 }
183 HostChargeReservationOutcome::Rejected { reason } => {
184 return Err(SubscriptionBillingServiceError::HostChargeReservationRejected(reason));
185 }
186 HostChargeReservationOutcome::Reserved(_) => {
187 return Err(SubscriptionBillingServiceError::InvalidState(
188 INVALID_SERVICE_STATE,
189 ));
190 }
191 };
192 if attempt.identity().attempt_id() != candidate_id {
193 reservation = HostChargeReservation::from_attempt(&attempt).map_err(|_| {
194 SubscriptionBillingServiceError::InvalidState(INVALID_SERVICE_STATE)
195 })?;
196 if reservation.identity().required_gateway_account_mode()
197 != self.required_gateway_account_mode
198 {
199 return Err(SubscriptionBillingServiceError::GatewayConfigurationChanged);
200 }
201 if reservation.identity().gateway_account_id() != gateway.gateway_account_id()
202 || reservation.request().gateway_order_id()
203 != &gateway.mutation_reference_factory().for_attempt(
204 PaymentAttemptKind::HostCharge,
205 reservation.identity().attempt_id(),
206 )
207 {
208 return Err(SubscriptionBillingServiceError::InvalidState(
209 INVALID_SERVICE_STATE,
210 ));
211 }
212 }
213
214 let prepared_stage = HostChargePreSubmissionStage::Reserved {
215 targets,
216 reservation: &reservation,
217 resolution: HostChargeBeforeSubmissionResolution::prepared(),
218 };
219 match self.host_charge_cooldown(&account, prepared_stage).await? {
220 HostChargePreSubmissionOutcome::Ready(()) => {}
221 HostChargePreSubmissionOutcome::Resolved(payment) => return Ok(*payment),
222 }
223
224 let admission =
225 match admit_host_charge_submission(&self.pool, targets, &reservation).await? {
226 HostChargeAdmissionOutcome::Admitted(admission) => *admission,
227 HostChargeAdmissionOutcome::AlreadyAdmitted(attempt) => {
228 return host_charge_payment_result(attempt);
229 }
230 HostChargeAdmissionOutcome::Rejected { attempt, .. } => {
231 return host_charge_payment_result(attempt);
232 }
233 };
234 let admitted_stage = HostChargePreSubmissionStage::Reserved {
235 targets,
236 reservation: &reservation,
237 resolution: HostChargeBeforeSubmissionResolution::admitted_not_submitted(),
238 };
239 match self.host_charge_cooldown(&account, admitted_stage).await? {
240 HostChargePreSubmissionOutcome::Ready(()) => {}
241 HostChargePreSubmissionOutcome::Resolved(payment) => return Ok(*payment),
242 }
243 match submit_admitted_host_charge(
244 &self.pool,
245 self.coordinator.as_ref(),
246 targets,
247 admission,
248 &command,
249 verified_gateway,
250 )
251 .await?
252 {
253 HostChargeProviderResult::Payment(payment) => Ok(payment),
254 HostChargeProviderResult::NotSubmitted { error, .. } => {
255 Err(SubscriptionBillingServiceError::GatewayNotSubmitted(error))
256 }
257 }
258 }
259
260 pub async fn apply_reconciled_host_charge_outcome(
262 &self,
263 billing_scope_id: BillingScopeId,
264 attempt_id: PaymentAttemptId,
265 outcome: &GatewayPaymentOutcome,
266 ) -> Result<HostChargePaymentResult, SubscriptionBillingServiceError> {
267 let targets = self
268 .host_charge_targets
269 .as_deref()
270 .ok_or(SubscriptionBillingServiceError::HostChargeUnavailable)?;
271 apply_reconciled_host_charge_gateway_outcome(
272 &self.pool,
273 self.coordinator.as_ref(),
274 targets,
275 billing_scope_id,
276 attempt_id,
277 outcome,
278 )
279 .await
280 .map_err(Into::into)
281 }
282
283 pub(super) async fn preflight_host_charge(
284 &self,
285 targets: &dyn HostChargeTargetStore,
286 command: &ChargeHostTarget,
287 ) -> Result<HostChargePreflightOutcome, SubscriptionBillingServiceError> {
288 let mut transaction = self.pool.begin().await?;
289 let outcome = preflight_host_charge_in_transaction(
290 &mut transaction,
291 targets,
292 command,
293 self.required_gateway_account_mode,
294 )
295 .await?;
296 transaction.commit().await?;
297 Ok(outcome)
298 }
299
300 pub(super) async fn reserve_host_charge(
301 &self,
302 targets: &dyn HostChargeTargetStore,
303 reservation: &HostChargeReservation,
304 ) -> Result<HostChargeReservationOutcome, SubscriptionBillingServiceError> {
305 let mut transaction = self.pool.begin().await?;
306 let outcome =
307 reserve_host_charge_in_transaction(&mut transaction, targets, reservation).await?;
308 transaction.commit().await?;
309 Ok(outcome)
310 }
311
312 async fn host_charge_cooldown(
313 &self,
314 account: &GatewayAccountSnapshot,
315 stage: HostChargePreSubmissionStage<'_>,
316 ) -> Result<HostChargePreSubmissionOutcome, SubscriptionBillingServiceError> {
317 if let Some(scope) = self.active_cooldown(account).await? {
318 return match stage {
319 HostChargePreSubmissionStage::Unreserved => {
320 Err(SubscriptionBillingServiceError::GatewayMutationCooldown { scope })
321 }
322 HostChargePreSubmissionStage::Reserved {
323 targets,
324 reservation,
325 resolution,
326 } => self
327 .resolve_host_charge_cooldown(
328 targets,
329 reservation,
330 &account.provider_key,
331 scope,
332 resolution,
333 )
334 .await
335 .map(HostChargePreSubmissionOutcome::resolved),
336 };
337 }
338 Ok(HostChargePreSubmissionOutcome::Ready(()))
339 }
340
341 async fn host_charge_unreserved_gateway_readiness<'gateway>(
342 &self,
343 account: &GatewayAccountSnapshot,
344 gateway: &'gateway syrup_rail::ResolvedGateway,
345 ) -> Result<ModeVerifiedGateway<'gateway>, SubscriptionBillingServiceError> {
346 match verify_gateway_account_mode(gateway, self.required_gateway_account_mode).await {
347 Ok(verified_gateway) => Ok(verified_gateway),
348 Err(GatewayAccountModeVerificationError::AccountModeMismatch { .. }) => {
349 Err(SubscriptionBillingServiceError::GatewayReadiness(
350 GatewayError::Configuration(gateway_account_mode_mismatch_detail()),
351 ))
352 }
353 Err(GatewayAccountModeVerificationError::Gateway(GatewayError::RateLimited(_))) => {
354 self.extend_provider_cooldown(
355 gateway.billing_scope_id(),
356 gateway.gateway_account_id(),
357 &account.provider_key,
358 )
359 .await?;
360 Err(SubscriptionBillingServiceError::GatewayMutationCooldown {
361 scope: GatewayMutationCooldownScope::Provider,
362 })
363 }
364 Err(GatewayAccountModeVerificationError::Gateway(error)) => {
365 Err(SubscriptionBillingServiceError::GatewayReadiness(error))
366 }
367 }
368 }
369
370 async fn host_charge_prepared_gateway_readiness<'gateway>(
371 &self,
372 gateway: &'gateway syrup_rail::ResolvedGateway,
373 targets: &dyn HostChargeTargetStore,
374 reservation: &HostChargeReservation,
375 ) -> Result<
376 HostChargePreSubmissionOutcome<ModeVerifiedGateway<'gateway>>,
377 SubscriptionBillingServiceError,
378 > {
379 let readiness =
380 verify_gateway_account_mode(gateway, self.required_gateway_account_mode).await;
381 let resolution = HostChargeBeforeSubmissionResolution::prepared();
382 match readiness {
383 Ok(verified_gateway) => Ok(HostChargePreSubmissionOutcome::Ready(verified_gateway)),
384 Err(GatewayAccountModeVerificationError::AccountModeMismatch { required, .. }) => {
385 let detail = gateway_account_mode_mismatch_detail();
386 let policy = GatewayNotSubmittedPolicy::for_account_mode_mismatch(required);
387 let code = policy.resolution_code();
388 let payment = self
389 .resolve_host_charge_readiness(
390 targets,
391 reservation,
392 gateway.provider_key(),
393 detail.clone(),
394 code,
395 resolution.with_not_submitted_policy(policy),
396 )
397 .await?;
398 if payment.attempt().state().resolution_code() == Some(code) {
399 Err(SubscriptionBillingServiceError::GatewayReadiness(
400 GatewayError::Configuration(detail),
401 ))
402 } else {
403 Ok(HostChargePreSubmissionOutcome::resolved(payment))
404 }
405 }
406 Err(GatewayAccountModeVerificationError::Gateway(error)) => {
407 let policy = GatewayNotSubmittedPolicy::for_readiness_error(&error);
408 if policy.restores_prepared_attempt_when_supported() {
409 return Err(SubscriptionBillingServiceError::GatewayReadiness(error));
410 }
411 let resolution = resolution.with_not_submitted_policy(policy);
412 if let Some(cooldown) = policy.cooldown() {
413 return self
414 .resolve_host_charge_cooldown_with_detail(
415 targets,
416 reservation,
417 gateway.provider_key(),
418 error.detail().clone(),
419 HostChargeCooldownSurface {
420 scope: GatewayMutationCooldownScope::from_rate_limit_cooldown(
421 cooldown,
422 ),
423 code: policy.resolution_code(),
424 },
425 resolution,
426 )
427 .await
428 .map(HostChargePreSubmissionOutcome::resolved);
429 }
430 let code = policy.resolution_code();
431 let payment = self
432 .resolve_host_charge_readiness(
433 targets,
434 reservation,
435 gateway.provider_key(),
436 error.detail().clone(),
437 code,
438 resolution,
439 )
440 .await?;
441 if payment.attempt().state().resolution_code() == Some(code) {
442 Err(SubscriptionBillingServiceError::GatewayReadiness(error))
443 } else {
444 Ok(HostChargePreSubmissionOutcome::resolved(payment))
445 }
446 }
447 }
448 }
449
450 pub(super) async fn resolve_host_charge_readiness(
451 &self,
452 targets: &dyn HostChargeTargetStore,
453 reservation: &HostChargeReservation,
454 provider_key: &GatewayProviderKey,
455 detail: GatewayDiagnostic,
456 code: PaymentResolutionCode,
457 resolution: HostChargeBeforeSubmissionResolution,
458 ) -> Result<HostChargePaymentResult, SubscriptionBillingServiceError> {
459 resolve_host_charge_before_submission(
460 &self.pool,
461 targets,
462 reservation,
463 provider_key,
464 detail,
465 code,
466 resolution,
467 )
468 .await
469 .map_err(Into::into)
470 }
471
472 pub(super) async fn resolve_host_charge_cooldown(
473 &self,
474 targets: &dyn HostChargeTargetStore,
475 reservation: &HostChargeReservation,
476 provider_key: &GatewayProviderKey,
477 scope: GatewayMutationCooldownScope,
478 resolution: HostChargeBeforeSubmissionResolution,
479 ) -> Result<HostChargePaymentResult, SubscriptionBillingServiceError> {
480 let provider_name = provider_key.as_str().to_ascii_uppercase();
481 let detail = match scope {
482 GatewayMutationCooldownScope::Account => GatewayDiagnostic::new(&format!(
483 "{provider_name} account mutation cooldown is active."
484 )),
485 GatewayMutationCooldownScope::Provider => GatewayDiagnostic::new(&format!(
486 "{provider_name} system provider cooldown is active."
487 )),
488 };
489 let code = match scope {
490 GatewayMutationCooldownScope::Account => {
491 PaymentResolutionCode::GatewayAccountMutationCooldownBeforeSubmission
492 }
493 GatewayMutationCooldownScope::Provider => {
494 PaymentResolutionCode::GatewayProviderRateLimitedBeforeSubmission
495 }
496 };
497 self.resolve_host_charge_cooldown_with_detail(
498 targets,
499 reservation,
500 provider_key,
501 detail,
502 HostChargeCooldownSurface { scope, code },
503 resolution,
504 )
505 .await
506 }
507
508 async fn resolve_host_charge_cooldown_with_detail(
509 &self,
510 targets: &dyn HostChargeTargetStore,
511 reservation: &HostChargeReservation,
512 provider_key: &GatewayProviderKey,
513 detail: GatewayDiagnostic,
514 cooldown: HostChargeCooldownSurface,
515 resolution: HostChargeBeforeSubmissionResolution,
516 ) -> Result<HostChargePaymentResult, SubscriptionBillingServiceError> {
517 let payment = self
518 .resolve_host_charge_readiness(
519 targets,
520 reservation,
521 provider_key,
522 detail,
523 cooldown.code,
524 resolution,
525 )
526 .await?;
527 if payment.attempt().state().resolution_code() == Some(cooldown.code) {
528 Err(SubscriptionBillingServiceError::GatewayMutationCooldown {
529 scope: cooldown.scope,
530 })
531 } else {
532 Ok(payment)
533 }
534 }
535}