# frontend/platform_commerce_api.py at origin/main e40b63b7 (before the two-tier merge):
# _emit_refund_webhook and _refund_card, VERBATIM. tests/test_platform_commerce_api.py
# runs this against the same seams as the current code to pin that a one-shot
# full card refund answers and emits byte-for-byte what main did. Never edit.
def _emit_refund_webhook(game, order, player, amount, funding, unit, dev_reversed=None,
                         tax_reversed=None, tax_jurisdiction=None):
    try:
        from .services.webhook_emitter import emit_event
        from .utils.identity_hash import derive_identity_id
        emit_event(
            event_type='platform_commerce.refunded', tenant_id=game.game_id,
            data={
                'order_id': order.order_id, 'funding_source': funding,
                'player_email': player.player_email if player else None,
                'identity_id': (derive_identity_id(player.universal_player_hash) if player else None),
                'refunded_amount': str(amount), 'amount_unit': unit,
                'fee_retained': True,
                # The partner's revenue on this sale is reversed (INVO's fee is not);
                # partners reconcile their payable against this.
                'developer_revenue_reversed': (
                    str(dev_reversed) if dev_reversed is not None else None),
                # ADDITIVE. Consumption tax goes back to the buyer IN FULL on
                # whatever is refunded: it was never INVO's and it is not the
                # partner's, so it appears beside the revenue reversal rather
                # than inside it. Zero on the balance leg, which moves coins and
                # no fiat, and zero everywhere until a registration exists.
                'tax_refunded_usd': (
                    str(tax_reversed) if tax_reversed is not None else '0.00'),
                'tax_jurisdiction': tax_jurisdiction,
            },
        )
    except Exception as wh_e:
        logger.warning(f"webhook_emit platform_commerce.refunded failed: {wh_e}")




def _refund_card(game, order, player, reason, now_utc, actor='platform'):
    """Card leg refund: Stripe-refund the full item price to the customer."""
    import stripe
    if not order.payment_reference:
        return no_cache_response({"message": "Order has no payment reference to refund"}, 400)

    stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
    try:
        @platform_commerce_breaker
        def _create_refund():
            # Idempotency across the >24h Stripe idempotency-key window: if a refund
            # for this PaymentIntent already exists (e.g. a prior attempt issued the
            # Stripe refund but crashed before the DB commit), REUSE it instead of
            # creating a second money-out. Covers the post-Stripe/pre-commit crash.
            existing = stripe.Refund.list(payment_intent=order.payment_reference, limit=1)
            data = getattr(existing, 'data', None) or []
            if data:
                return data[0]
            return stripe.Refund.create(
                payment_intent=order.payment_reference,
                reason='requested_by_customer',
                idempotency_key=f"pc_refund:{order.order_id}",
                metadata={'flow': 'platform_commerce_refund', 'order_id': str(order.order_id)},
            )
        refund = _create_refund()
    except pybreaker.CircuitBreakerError:
        db.session.rollback()
        return no_cache_response({
            "error": "service_unavailable",
            "message": "Refunds are temporarily unavailable. Please try again in a moment.",
        }, 503)
    except Exception as stripe_e:
        db.session.rollback()
        logger.error(f"platform commerce Stripe refund failed: {stripe_e}")
        return no_cache_response({"message": "Refund could not be processed at the processor"}, 502)

    # THE WHOLE CHARGE GOES BACK, tax included: `_create_refund` above refunds
    # the PaymentIntent with no amount, so the processor returns everything that
    # was taken. Split out here because the two halves land in different places
    # -- the cash leg reverses out of the clearing account, the tax DEBITS 2300
    # Tax Payable -- and because the refund Transaction should mirror the sale
    # Transaction, which records the item price and not somebody's VAT.
    amount = order.usd_amount or Decimal('0.00')
    tax_reversed = Decimal(str(order.tax_amount_usd or 0))
    if tax_reversed > amount:
        tax_reversed = Decimal('0.00')
    sale_refunded = amount - tax_reversed
    txn = _refund_txn(game, order, sale_refunded, 'platform_item_refund_card', now_utc)
    # Reverse the partner's payable for the undone sale (INVO's fee stays booked).
    # The card sale's rows are amount_unit='usd'; the reversal preserves that unit.
    clawback = reverse_developer_revenue(
        sale_transaction=order.transaction, refund_transaction=txn, now_utc=now_utc)
    order.mark_as_refunded(refund_ref=str(getattr(refund, 'id', '')), refund_amt=amount, actor=actor,
                           metadata={'reason': reason, 'stripe_refund_id': getattr(refund, 'id', None)})

    # --- GENERAL LEDGER (BE-PIP-008) --------------------------------------
    # AFTER the processor round trip, deliberately. `_create_refund` above is a
    # network call to Stripe, so the post sits here, below the refund, alongside
    # the other post-processor DB writes (_refund_txn, reverse_developer_revenue,
    # mark_as_refunded) and above the commit. It adds no DB work before the Stripe
    # call. Same guarantees as the balance leg: flag-gated, savepoint-contained,
    # non-fatal, idempotent on the refund transaction id.
    #   Do NOT read this as "no DB transaction spans the processor call here" —
    # that is true of this savepoint only. _process_refund takes the OrderBook row
    # lock (populate_existing().with_for_update()) before dispatching, and that
    # lock plus the open transaction IS held across stripe.Refund.list/create
    # above. A hung Stripe refund therefore pins this order row for the full
    # processor timeout. Pre-existing, single-row, refund-only; the fix is to
    # release before the call and re-lock after, which needs a re-check of
    # status=='completed' on the second lock.
    from .services.gl_posting import post_platform_commerce_card_refund
    gl_shadow_post(
        'platform_commerce_card_refund', post_platform_commerce_card_refund,
        txn.transaction_id,
        game_id=game.game_id,
        # USD on both figures — the card leg settles in fiat and nothing is
        # rescaled by the coin peg. The partner leg reads 'amount_usd' (via
        # fee_amount_usd), the mirror of the balance leg's 'amount_coins'.
        # No player_id: no PlayerBalance moved on either the sale or the refund.
        usd_refunded=amount,
        partner_reversed_usd=clawback['amount_usd'],
        # The tax was credited to 2300 when it was collected. The buyer has it
        # back, so INVO no longer owes it and the liability is discharged by the
        # same amount. Leaving this out would leave the books claiming a debt to
        # a revenue authority for money that is gone.
        tax_reversed_usd=tax_reversed,
    )
    # ON THE ORDER TOO. The ledger debit says INVO no longer owes it; this is
    # the subsidiary detail the remittance report reconciles against that
    # balance. Without it the report keeps claiming the full collection for ever.
    from .services.tax_service import record_tax_refunded
    record_tax_refunded(order, tax_reversed)

    _emit_refund_webhook(game, order, player, amount, FUNDING_CARD, 'usd',
                         dev_reversed=clawback['amount'],
                         tax_reversed=tax_reversed,
                         tax_jurisdiction=order.tax_jurisdiction)
    db.session.commit()

    return no_cache_response({
        "status": "refunded", "funding_source": FUNDING_CARD,
        "order_id": order.order_id, "transaction_id": txn.transaction_id,
        "refunded_amount_usd": str(amount), "stripe_refund_id": getattr(refund, 'id', None),
        # ADDITIVE. `refunded_amount_usd` keeps meaning the total that went back
        # to the card; this says how much of it was tax, so a partner can see
        # their revenue reversal and the tax reversal are different money.
        "tax_refunded_usd": str(tax_reversed),
        # Same value under a provider-neutral name so public docs have a key they
        # can print. `stripe_refund_id` stays — partners read it today.
        "processor_refund_reference": getattr(refund, 'id', None),
        "fee_retained": True,
        "developer_revenue_reversed_usd": str(clawback['amount']),
    }, 200)


