feat(billing): _api_cancel_subscription service method + unit tests

Plan 01 (NexaCloud cutover) Task 1: cancel/close a subscription with the same
service-scoped authorization as _api_record_usage (resolve via
_fc_resolve_subscription; partner must be linked to this service). Idempotent
(no-op if already 6_churn). 5 unit tests, verified green on fcb_test
(fresh + l10n_ca). DELETE route + HttpCase follow in Task 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-02 09:08:37 -04:00
parent 11ab261ad9
commit f67cefc213
3 changed files with 76 additions and 0 deletions

View File

@@ -247,3 +247,24 @@ class FusionBillingService(models.Model):
sub.action_confirm()
return {'status': 'ok', 'subscription_id': sub.id,
'subscription_state': sub.subscription_state}
def _api_cancel_subscription(self, external_ref):
"""Cancel (close) the subscription identified by ``external_ref``.
Authorization mirrors ``_api_record_usage``: the resolved sale.order must
exist, be a subscription, and belong to a customer THIS service is linked
to. Idempotent — closing an already-churned subscription returns ok.
Validation (C3): an empty ref returns a 4xx-shaped error, never raises.
"""
self.ensure_one()
if external_ref in (None, ''):
return {'status': 'error', 'error': 'subscription id required'}
sub = self._fc_resolve_subscription(external_ref)
linked_partners = self.account_link_ids.mapped('partner_id')
if not sub.exists() or not sub.is_subscription \
or sub.partner_id not in linked_partners:
return {'status': 'error', 'error': 'unknown subscription'}
if sub.subscription_state != '6_churn':
sub.set_close()
return {'status': 'ok', 'subscription_id': sub.id,
'subscription_state': sub.subscription_state}