# Path: billing/actions/accounts.py
# def add_charge(account_id: str,
#                amount: Money,
#                reverses_id: Optional[str] = None,
#                product_code: Optional[str] = None,
#                product_properties: Optional[Dict[str, str]] = None) -> Charge:
#     """
#     Add a charge to the account.
# 
#     :param account_id: The account on which to add the charge
#     :param amount:  The amount of the charge
#     :param reverses_id: Set this if this charge reverses another one
#     :param product_code: A code identifying the type of product cnarged
#     :param product_properties: A dict of hames and values.
#     :return: The newly created charge.
#     """
#     logger.info('adding-charge', account_id=account_id, amount=amount, product_code=product_code,
#                 product_properties=product_properties)
# 
#     with transaction.atomic():
#         charge = Charge(account_id=account_id,
#                         amount=amount)
#         if reverses_id:
#             charge.reverses_id = reverses_id
#         if product_code:
#             charge.product_code = product_code
#         charge.full_clean(exclude=['id', 'account'])  # Exclude to avoid unnecessary db queries
#         charge.save(force_insert=True)
# 
#         if product_properties:
#             objs = [ProductProperty(charge=charge, name=k, value=v) for k, v in product_properties.items()]
#             for o in objs:
#                 o.full_clean(exclude=['id', 'charge'])  # Exclude to avoid unnecessary db queries
#             ProductProperty.objects.bulk_create(objs)
# 
#     return charge
# 
# Path: billing/models.py
# class Charge(Model):
#     """
#     A charge has a signed amount. If the amount is negative then the charge is in fact a credit.
#     """
#     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
#     created = models.DateTimeField(auto_now_add=True, db_index=True)
#     modified = models.DateTimeField(auto_now=True)
#     account = models.ForeignKey(Account, on_delete=PROTECT, related_name='charges')
#     invoice = models.ForeignKey(Invoice, null=True, blank=True, related_name='items', on_delete=SET_NULL)
#     amount = MoneyField(max_digits=12, decimal_places=2)
#     ad_hoc_label = models.TextField(blank=True, help_text='When not empty, this is shown verbatim to the user.')
#     product_code = models.CharField(max_length=20, blank=True, validators=[product_code_validator], db_index=True,
#                                     help_text='Identifies the kind of product being charged or credited')
#     reverses = models.OneToOneField('self', null=True, blank=True, related_name='reversed_by', on_delete=PROTECT)
#     deleted = models.BooleanField(default=False, db_index=True)
# 
#     objects = ChargeManager()
# 
#     all_charges = models.Manager()  # Includes deleted charges
# 
#     def clean(self):
#         if not (self.ad_hoc_label or self.product_code):
#             raise ValidationError('Either the ad-hoc-label or the product-code must be filled.')
# 
#     @property
#     def type(self):
#         a = self.amount.amount
#         if a >= 0:
#             return _('Charge')
#         else:
#             return _('Credit')


from django.db import transaction
from structlog import get_logger
from .accounts import add_charge
from ..models import Charge


logger = get_logger()

REVERSAL_PRODUCT_CODE = 'REVERSAL'


class ChargeAlreadyCancelledError(Exception):
    pass


def cancel_charge(charge_id: str) -> None:
    """
    Cancels an existing charge.

    If the charge was already cancelled then an Exception is raised.

    If it is not in an invoice then the charge is deleted,
    otherwise a Credit object is created to reverse the Charge.

    :param charge_id: The id of the charge to cancel.
    """
    logger.info('cancelling-charge', charge_id=charge_id)

    with transaction.atomic():
        charge = Charge.all_charges.get(pk=charge_id)

        if charge.deleted:
            raise ChargeAlreadyCancelledError("Cannot cancel deleted charge.")


# Path: billing/actions/accounts.py
# def add_charge(account_id: str,
#                amount: Money,
#                reverses_id: Optional[str] = None,
#                product_code: Optional[str] = None,
#                product_properties: Optional[Dict[str, str]] = None) -> Charge:
#     """
#     Add a charge to the account.
# 
#     :param account_id: The account on which to add the charge
#     :param amount:  The amount of the charge
#     :param reverses_id: Set this if this charge reverses another one
#     :param product_code: A code identifying the type of product cnarged
#     :param product_properties: A dict of hames and values.
#     :return: The newly created charge.
#     """
#     logger.info('adding-charge', account_id=account_id, amount=amount, product_code=product_code,
#                 product_properties=product_properties)
# 
#     with transaction.atomic():
#         charge = Charge(account_id=account_id,
#                         amount=amount)
#         if reverses_id:
#             charge.reverses_id = reverses_id
#         if product_code:
#             charge.product_code = product_code
#         charge.full_clean(exclude=['id', 'account'])  # Exclude to avoid unnecessary db queries
#         charge.save(force_insert=True)
# 
#         if product_properties:
#             objs = [ProductProperty(charge=charge, name=k, value=v) for k, v in product_properties.items()]
#             for o in objs:
#                 o.full_clean(exclude=['id', 'charge'])  # Exclude to avoid unnecessary db queries
#             ProductProperty.objects.bulk_create(objs)
# 
#     return charge
# 
# Path: billing/models.py
# class Charge(Model):
#     """
#     A charge has a signed amount. If the amount is negative then the charge is in fact a credit.
#     """
#     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
#     created = models.DateTimeField(auto_now_add=True, db_index=True)
#     modified = models.DateTimeField(auto_now=True)
#     account = models.ForeignKey(Account, on_delete=PROTECT, related_name='charges')
#     invoice = models.ForeignKey(Invoice, null=True, blank=True, related_name='items', on_delete=SET_NULL)
#     amount = MoneyField(max_digits=12, decimal_places=2)
#     ad_hoc_label = models.TextField(blank=True, help_text='When not empty, this is shown verbatim to the user.')
#     product_code = models.CharField(max_length=20, blank=True, validators=[product_code_validator], db_index=True,
#                                     help_text='Identifies the kind of product being charged or credited')
#     reverses = models.OneToOneField('self', null=True, blank=True, related_name='reversed_by', on_delete=PROTECT)
#     deleted = models.BooleanField(default=False, db_index=True)
# 
#     objects = ChargeManager()
# 
#     all_charges = models.Manager()  # Includes deleted charges
# 
#     def clean(self):
#         if not (self.ad_hoc_label or self.product_code):
#             raise ValidationError('Either the ad-hoc-label or the product-code must be filled.')
# 
#     @property
#     def type(self):
#         a = self.amount.amount
#         if a >= 0:
#             return _('Charge')
#         else:
#             return _('Credit')


# Path: billing/actions/charges.py
from django.db import transaction
from structlog import get_logger
from .accounts import add_charge
from ..models import Charge



logger = get_logger()

REVERSAL_PRODUCT_CODE = 'REVERSAL'


class ChargeAlreadyCancelledError(Exception):
    pass


def cancel_charge(charge_id: str) -> None:
    """
    Cancels an existing charge.

    If the charge was already cancelled then an Exception is raised.

    If it is not in an invoice then the charge is deleted,
    otherwise a Credit object is created to reverse the Charge.

    :param charge_id: The id of the charge to cancel.
    """
    logger.info('cancelling-charge', charge_id=charge_id)

    with transaction.atomic():
        charge = Charge.all_charges.get(pk=charge_id)

        if charge.deleted:
            raise ChargeAlreadyCancelledError("Cannot cancel deleted charge.")


        if Charge.all_charges.filter(reverses=charge_id).exists():