Example #1
0
async def _(db=db):
    customer = await StripeCustomerFactory(user_id=1)

    repository = AssociationMembershipRepository()
    found_customer = await repository.get_stripe_customer_from_user_id(1)

    assert found_customer.id == customer.id
Example #2
0
async def _(db=db):
    subscription = await SubscriptionFactory(user_id=1)

    repository = AssociationMembershipRepository()
    found_subscription = await repository.get_user_subscription(1)

    assert found_subscription.id == subscription.id
async def _(db=db):
    repository = AssociationMembershipRepository()
    with time_machine.travel("2020-10-10 10:00:00", tick=False):
        subscription_1 = await SubscriptionFactory(
            user_id=1, status=SubscriptionStatus.CANCELED)

        await repository.save_subscription(subscription_1)

        await membership_check_status({})

        updated_subscription_1 = await Subscription.objects.get_or_none(
            id=subscription_1.id)
        assert updated_subscription_1.status == SubscriptionStatus.CANCELED
async def _(db=db):
    repository = AssociationMembershipRepository()

    with time_machine.travel("2020-10-10 10:00:00", tick=False):
        subscription_1 = await SubscriptionFactory(
            user_id=1, status=SubscriptionStatus.ACTIVE)
        subscription_1.add_pretix_payment(
            organizer="python-italia",
            event="pycon-demo",
            order_code="XXYYZZ",
            total=1000,
            status=PaymentStatus.CANCELED,
            payment_date=datetime.datetime(2019,
                                           10,
                                           10,
                                           1,
                                           4,
                                           43,
                                           tzinfo=timezone.utc),
            period_start=datetime.datetime(2019,
                                           10,
                                           10,
                                           1,
                                           4,
                                           43,
                                           tzinfo=timezone.utc),
            period_end=datetime.datetime(2022,
                                         10,
                                         10,
                                         1,
                                         4,
                                         43,
                                         tzinfo=timezone.utc),
        )

        await repository.save_subscription(subscription_1)

        await membership_check_status({})

        updated_subscription_1 = await Subscription.objects.get_or_none(
            id=subscription_1.id)
        assert updated_subscription_1.status == SubscriptionStatus.CANCELED
async def update_expired_subscriptions():
    repository = AssociationMembershipRepository()

    now = datetime.now(timezone.utc)
    # Ideally in the future we should use the psql NOW() function
    qs = Subscription.objects.filter(status=SubscriptionStatus.ACTIVE,).exclude(
        payments__status=PaymentStatus.PAID,
        payments__period_start__lte=now,
        payments__period_end__gte=now,
    )
    subscriptions_to_cancel = await qs.all()
    subscriptions_to_cancel_count = await qs.count()

    logger.info(
        "Found subscriptions_to_cancel_count=%s subscriptions to cancel",
        subscriptions_to_cancel_count,
    )

    for subscription in subscriptions_to_cancel:
        subscription.mark_as_canceled()
        await repository.save_subscription(subscription)
async def update_now_active_subscriptions():
    repository = AssociationMembershipRepository()

    now = datetime.now(timezone.utc)
    # Ideally in the future we should use the psql NOW() function
    qs = Subscription.objects.filter(
        status=SubscriptionStatus.CANCELED,
        payments__status=PaymentStatus.PAID,
        payments__period_start__lte=now,
        payments__period_end__gte=now,
    )
    subscriptions_to_enable = await qs.all()
    subscriptions_to_enable_count = await qs.count()

    logger.info(
        "Found subscriptions_to_enable_count=%s subscriptions to activate",
        subscriptions_to_enable_count,
    )

    for subscription in subscriptions_to_enable:
        subscription.mark_as_active()
        await repository.save_subscription(subscription)
Example #7
0
 def association_repository(self) -> AssociationMembershipRepository:
     return AssociationMembershipRepository()
async def handle_invoice_paid(event):
    invoice = event.data.object
    stripe_customer_id = invoice.customer
    stripe_subscription_id = invoice.subscription

    # Take the first item they purchased
    # users can only buy the subscription
    # so the lines will always be 1
    # If we change and allow people to buy
    # multiple this we need to update this
    assert (len(invoice.lines.data) == 1
            ), f"event_id={event.id} has more items than excepted"
    assert (invoice.status == "paid"
            ), f"event_id={event.id} has invoice_status={invoice.status}"

    membership_repository = AssociationMembershipRepository()
    subscription = await membership_repository.get_subscription_from_stripe_customer(
        stripe_customer_id)

    if not subscription:
        logger.error(
            "Unable to process stripe event_id=%s invoice paid because stripe_customer_id=%s"
            " doesn't have an associated Customer locally or a subscription",
            event.id,
            stripe_customer_id,
        )
        raise NoCustomerFoundForEvent()

    if await membership_repository.is_payment_already_processed(invoice.id):
        logger.info(
            "Ignoring event_id=%s from Stripe because we already processed "
            "the payment of invoice_id=%s",
            event.id,
            invoice.id,
        )
        return

    invoice_period = invoice.lines.data[0].period

    period_start = datetime.fromtimestamp(invoice_period.start,
                                          tz=timezone.utc)
    period_end = datetime.fromtimestamp(invoice_period.end, tz=timezone.utc)
    now = datetime.now(timezone.utc)

    subscription.add_stripe_subscription_payment(
        total=invoice.total,
        status=PaymentStatus.PAID,
        payment_date=datetime.fromtimestamp(invoice.status_transitions.paid_at,
                                            tz=timezone.utc),
        period_start=period_start,
        period_end=period_end,
        stripe_subscription_id=stripe_subscription_id,
        stripe_invoice_id=invoice.id,
        invoice_pdf=invoice.invoice_pdf,
    )

    # If the payment we just received is for the current
    # period, we mark the subscription as active
    if period_start <= now <= period_end:
        subscription.mark_as_active()
    await membership_repository.save_subscription(subscription)
async def pretix_event_order_paid(payload):
    action = payload["action"]
    organizer = payload["organizer"]
    event = payload["event"]
    order_code = payload["code"]

    pretix_api = PretixAPI(organizer, event)

    order_data = pretix_api.get_order_data(order_code)

    invalid_order_statuses = ["n", "e", "c"]
    order_status = order_data["status"]
    if order_status in invalid_order_statuses:
        logger.error(
            "Received a paid order event for order_code=%s but the order_status=%s "
            "we only want paid orders",
            order_code,
            order_status,
        )
        return

    categories = pretix_api.get_categories()["results"]
    association_category = next(
        (category for category in categories
         if category["internal_name"] == "Association"),
        None,
    )

    if not association_category:
        logger.info(
            "Ignoring order_code=%s paid event for organizer=%s event=%s "
            "because there isn't an association category",
            order_code,
            organizer,
            event,
        )
        return

    valid_items = pretix_api.get_items(qs={
        "category": association_category["id"],
        "active": "true"
    })
    valid_items_ids = [item["id"] for item in valid_items["results"]]

    order_positions = order_data["positions"]
    membership_positions = []
    for position in order_positions:
        if position["item"] in valid_items_ids:
            membership_positions.append(position)

    if not membership_positions:
        logger.info(
            "No membership positions for order_code=%s so nothing to do",
            order_code)
        return

    if len(membership_positions) > 1:
        logger.error(
            "Multiple positions found in order_code=%s (organizer=%s event=%s) that subscribe "
            "the user to the association. "
            "This is not supported and needs to be refunded or manually handled",
            order_code,
            organizer,
            event,
        )
        raise UnsupportedMultipleMembershipInOneOrder(
            f"Multiple positions found in order_code={order_code} that subscribe the user to the association. This is not supported."
        )

    user_email = order_data["email"]
    client = ServiceClient(
        url=f"{USERS_SERVICE_URL}/internal-api",
        service_name="users-backend",
        caller="association-backend",
        jwt_secret=str(SERVICE_TO_SERVICE_SECRET),
    )
    result = await client.execute(GET_USER_ID_BY_EMAIL, {"email": user_email})
    data = result.data

    if not data["userByEmail"]:
        raise NoUserFoundWithEmail(
            f"No user found with the email of order_code={order_code}")

    user_id = int(data["userByEmail"]["id"])
    repository = AssociationMembershipRepository()

    idempotency_key = PretixPayment.generate_idempotency_key(
        organizer, event, order_code)
    if await repository.is_payment_already_processed(idempotency_key):
        logger.info(
            "Ignoring action=%s (organizer=%s event=%s) from Pretix because we already processed "
            "the payment with key=%s from order_code=%s",
            action,
            organizer,
            event,
            idempotency_key,
            order_code,
        )
        return

    subscription = await repository.get_user_subscription(user_id)

    if not subscription:
        subscription = await repository.create_subscription(user_id)

    if subscription.is_active:
        logger.error(
            "user_id=%s is already subscribed to the association "
            "but paid a subscription via order_code=%s (organizer=%s event=%s)!",
            user_id,
            order_code,
            organizer,
            event,
        )
        raise UserIsAlreadyAMember(
            "User is already subscribed to the association")

    membership_position = membership_positions[0]
    membership_price = Decimal(membership_position["price"])
    paid_payments = [
        payment for payment in order_data["payments"]
        if payment["state"] == "confirmed"
    ]

    if not paid_payments:
        logger.error(
            "user_id=%s is already subscribed to the association "
            "but paid a subscription via order_code=%s (organizer=%s event=%s)!",
            user_id,
            order_code,
            organizer,
            event,
        )
        raise NoConfirmedPaymentFound(
            f"No confirmed payment found for order_code={order_code}")

    total_refunded = sum([
        Decimal(refund["amount"]) for refund in order_data["refunds"]
        if refund["state"] == "done"
    ])
    total_paid = sum([Decimal(payment["amount"]) for payment in paid_payments])

    if (total_paid - total_refunded) < membership_price:
        logger.error(
            "Received event paid for order_code=%s (organizer=%s event=%s) "
            "but the total_paid=%s (total_refunded=%s) doesn't cover the membership_price=%s "
            " so the membership cannot be created.",
            order_code,
            organizer,
            event,
            total_paid,
            total_refunded,
            membership_price,
        )
        raise NotEnoughPaid(
            f"Not enough payments found for order_code={order_code} to cover total={membership_price}"
        )

    payment_date = next(
        (payment["payment_date"]
         for payment in paid_payments if payment["payment_date"]),
        None,
    )
    if not payment_date:
        raise ValueError(f"No payment date for order_code={order_code}")

    payment_date = parser.parse(payment_date)
    period_start = payment_date
    period_end = payment_date + relativedelta(years=+1)

    # We assume our currency is EUR that has 2 decimal places and works in cents
    total = int(membership_price * 10**2)
    logger.info(
        "Adding new pretix payment to user_id=%s "
        "for period_start=%s to period_end=%s for order_code=%s organizer=%s event=%s",
        user_id,
        period_start,
        period_end,
        order_code,
        organizer,
        event,
    )
    subscription.add_pretix_payment(
        organizer=organizer,
        event=event,
        order_code=order_code,
        total=total,
        status=PaymentStatus.PAID,
        payment_date=payment_date,
        period_start=period_start,
        period_end=period_end,
    )

    # If the payment we just received is for the current
    # period, we mark the subscription as active
    now = datetime.now(timezone.utc)
    if period_start <= now <= period_end:
        subscription.mark_as_active()

    await repository.save_subscription(subscription)