Compare commits

..

No commits in common. "master" and "FEATURE/forced-transfer-mode" have entirely different histories.

133 changed files with 1862 additions and 11245 deletions

View file

@ -1,48 +0,0 @@
# No .env.example was previously tracked in this repo (.env is gitignored).
# This file documents every var read via decouple's config() in main/settings.py.
# Copy to .env and fill in real values before running the service.
SECRET_KEY=
DEBUG=True
DB_NAME=
DB_USER=
DB_PASSWORD=
DB_HOST=127.0.0.1
DB_PORT=5432
OAUTH2_PROVIDER_PUBLIC_URL=
OAUTH2_PROVIDER_PRIVATE_URL=
OAUTH2_CLIENT_ID=
OAUTH2_CLIENT_SECRET=
OAUTH2_SCOPES=
REDIS_BASE_URL=
LOKI_BASE_PUBLIC_URL=
MINIO_ENDPOINT=drive.gooyal.com
MINIO_USE_HTTPS=True
MINIO_EXTERNAL_ENDPOINT=drive.gooyal.com
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS=True
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
MINIO_MEDIA_FILES_BUCKET=
ACCOUNTS_BASE_PUBLIC_URL=
WALLET_BASE_PUBLIC_URL=
WALLET_RIAL_DEPOSIT=
WALLET_USER_BILLBOARD_VISIT_INCOME=
# NEW — wallet-naming refactor (2026-08-25), matches advertising/settlement/ipg.
# Do not reuse WALLET_RIAL_DEPOSIT or WALLET_USER_BILLBOARD_VISIT_INCOME here — this must
# be a distinct, dedicated platform wallet the wallet-service team provisions for promotions.
# TODO(you): replace with the real wallet-type UUID from the wallet service.
WALLET_PROMOTIONS_CREDIT=00000000-0000-0000-0000-000000000000
# NEW — per-promotion-type wallet routing (2026-08-25). Same wallet-service UUID as
# advertising's own WALLET_ADVERTISING_TRANSIT setting — copy that repo's real value here,
# don't provision a second one.
WALLET_ADVERTISING_TRANSIT=00000000-0000-0000-0000-000000000000
NOTIFICATIONS_BASE_PUBLIC_URL=

326
README.md
View file

@ -1,326 +0,0 @@
# Promotions
**Gooyal platform — service brief**
The reward-payout engine: it turns "a user did X" events from other Gooyal apps into real wallet deposits. This is a map of how it works internally and how it fits into the rest of the platform, written for the next person extending it.
`apps/promotions` · Django 5.1 · DRF · Postgres · Celery/Redis
---
## TL;DR
- **It's a rules engine, not a payment processor.** Other Gooyal apps report *events* ("user created their first ad"); promotions matches them against configured *plans* and decides who gets paid what.
- **It never moves money itself.** Every payout is a deposit submitted to the separate **wallet** service, then verified. Promotions only tracks whether that deposit succeeded.
- **Identity is borrowed, not owned.** Users and OAuth2 applications are lazily mirrored here from the central **accounts** service the moment they're first seen.
- **Two front doors, one engine.** A user-token API (self-service) and an application-token API (server-to-server, keyed by `user_uuid`) both funnel into the same `Plan` / `Recipient` / `Promotion` models.
---
## System map
Gooyal is a constellation of small Django services (accounts, wallet, notifications, advertising, chat, campaign, settlement, ipg…) that all authenticate through one central OAuth2 provider. Promotions is a consumer-facing resource server that in turn calls three of its siblings.
```mermaid
flowchart LR
classDef svc fill:#3550d6,stroke:#3550d6,color:#fff,rx:6,ry:6
classDef ext fill:#2e7d93,stroke:#2e7d93,color:#fff,rx:6,ry:6
classDef infra fill:#eeeeee,stroke:#999,color:#333,rx:6,ry:6
classDef caller fill:#ffffff,stroke:#888,color:#333,rx:6,ry:6
subgraph Callers[" calling applications, each with its own client-credentials token "]
direction TB
ADV["advertising"]:::caller
CHAT["chat"]:::caller
CAMP["campaign"]:::caller
SET["settlement"]:::caller
end
PROMO["promotions\n(this service)"]:::svc
ADV --> PROMO
CHAT --> PROMO
CAMP --> PROMO
SET --> PROMO
PROMO -- "introspect bearer token\n+ fetch user profile" --> ACC["accounts\nOAuth2 provider + identity\n(external, not in this checkout)"]:::ext
PROMO -- "submit + verify deposit" --> WAL["wallet\napplication & user ledgers\n(sibling repo)"]:::svc
PROMO -- "push notification\n(fire-and-forget)" --> NOT["notifications\n(external, not in this checkout)"]:::ext
PROMO -- "queue analyze_event_task" --> RED[("Redis")]:::infra
RED -- "Celery worker" --> PROMO
PROMO --- PG[("Postgres")]:::infra
```
Solid arrows are outbound REST calls made by promotions via generated OpenAPI clients in `utils/clients/`. Blue = Django services; teal = platform services whose source isn't checked out next to this repo.
---
## Identity & auth
Every request is a borrowed identity. Promotions has no login form and no password table that matters. It's a pure OAuth2 *resource server*: the **accounts** service is the one real authorization server on the platform, and every sibling service — promotions included — just validates tokens against it.
The `apps/gooyal_oauth2` app looks like a full OAuth2 provider (it defines `Application`, `AccessToken`, `Grant`, `RefreshToken`, `IDToken` models) but that's boilerplate shared across every Gooyal service, not a second identity system. In practice it's used as a **local cache**: `apps/gooyal_oauth2/validators.py` overrides django-oauth-toolkit's introspection so that the first time a bearer token is seen, promotions POSTs it to accounts's `/oauth2/introspect/`, then a second time to `/oauth2/introspect_application/`, and materializes the result as local `Application` / `AccessToken` rows. That's what lets `Plan.application` and `Promotion.application` be plain foreign keys instead of remote lookups on every access.
`apps/users.User` works the same way: a slim shadow row (UUID primary key, almost no other fields) created on first contact — either by the introspection validator (`get_or_create_user_from_content`) or explicitly in the application-token views, which call `utils/clients/accounts_client.get_user_info()` when a `user_uuid` in the URL doesn't have a local row yet.
Authorization itself is scope-based, checked per DRF view via two custom permission classes in `apps/gooyal_oauth2/rest_framework.py`: `IsAuthenticatedOrTokenMatchesOASRequirements` (plain endpoints) and `TokenMatchesViewSetActions` (viewsets, matches scopes per `action` rather than per HTTP method). Real scopes in use, pulled from the test suite:
| Scope | Grants |
|---|---|
| `promotions.application.event:submit` | App-to-app: submit an event on a user's behalf |
| `promotions.application.user-plan:promote` | App-to-app: trigger promotion evaluation for a plan directly |
| `promotions.application.user-plan:list-retrieve` | App-to-app: read plan/recipient configuration |
| `promotions.application.user-promotions:promote` | App-to-app: create promotions via `ApplicationPromoteUserApiView` |
| `promotions.application.user-promotions:list-retrieve` | App-to-app: list a user's promotion history |
| `promotions.user.self-plans:list-retrieve` | End user (session or user-token): browse their own plans |
---
## Domain model
Everything funnels through `apps/promotions/models.py`. It's compact — five real models — but `Recipient` carries a hand-rolled mini query language that's easy to misread the first time.
| Model | Role |
|---|---|
| `EventSaver` | Registers a valid `event_label` and whether it may only ever fire once per user (`save_once`). Acts as the allow-list for incoming events. |
| `Event` | One occurrence: `user` (raw UUID, not FK), `application`, `label`, free-form `data` JSON. GIN-indexed on `data`. |
| `Plan` | A campaign: a `balance`, the list of event labels that trigger it (`event_list`, GIN-indexed), a banner/description for display, and one or more `Recipient`s. |
| `Recipient` | Who gets paid from a `Plan` and how much — expressed as two small DSL strings rather than fixed fields (below). |
| `Promotion` | One payout attempt: state machine, links back to the triggering `Event`/`Plan`/`Recipient`, drives the real wallet deposit. |
### The recipient DSL
Two `Recipient` fields are tiny expression languages evaluated against whichever `Event` and `Plan` triggered the check.
`recipient_uuid_field` — who gets paid:
| Form | Meaning |
|---|---|
| `<a literal UUID>` | Fixed recipient, e.g. a promo operator account. |
| `->event:user` | The most common case: pay the user who fired the event. (`event.user` is read directly — no filter clause before the `->`.) |
| `model:key=value&…->model:key` | Conditional form: only resolves if every `key=value` check against `event`/`plan` (or their `data` JSON via `data__key`) matches; then reads the recipient off the named object. |
| `QS:Model:key=value&…->field` | Looks the *most recent* matching `Event`/`Plan`/`Promotion` row up by filter, then returns `field` as a literal UUID if it parses as one, else `row.data[field]`. |
`base_amount_field` is simpler: either a raw integer literal, or `event:key` / `plan:key` to pull the base amount out of that object's `data` JSON. `Recipient.data` then layers `promotion_percentage` (default 100) and an optional `max_promotion_amount` cap on top — see the watch list below for a live bug in that capping path.
---
## Request flow
From event to money in a wallet. The generic, asynchronous path: an app submits an event, promotions figures out the rest on a Celery worker. (The synchronous "promote this specific plan right now" endpoints skip the queue but land on the same `Recipient.promote()` call.)
```mermaid
sequenceDiagram
participant App as Calling app
participant Promo as promotions (API)
participant Q as Celery / Redis
participant Wal as wallet
participant Notif as notifications
App->>Promo: POST /api/v1/events/submit<br/>{label, data} + Bearer token
Promo->>Promo: EventSaver.save_event() → Event row
Promo-->>App: 201 Created
Promo->>Q: analyze_event_task.delay(event.uuid)
Q->>Promo: Event.analyze()
Promo->>Promo: Plan.objects.related_to_event(event)
loop each matching Plan
Promo->>Promo: Recipient.promote(plan, event)
Promo->>Promo: resolve DSL → recipient uuid + amount
Promo->>Promo: Plan.reserve_promotion_amount() (atomic)
Promo->>Wal: POST application/<wallet>/deposit/
Wal-->>Promo: transaction uuid, state=PENDING
Promo->>Wal: GET .../deposit/<uuid>/verify
Wal-->>Promo: state=SUCCESS
Promo->>Notif: POST push (best-effort, errors swallowed)
Promo->>Promo: Promotion.state → SUCCESS
end
```
`apps/promotions/tasks.py` · `apps/promotions/models.py` (`Event.analyze`, `Plan.process_event`, `Recipient.promote`, `Promotion.promote`)
---
## Payment states
One state machine, defined twice. `PaymentStateChoices` in promotions and `StateChoices` in wallet share the same integer values (1–7) by convention, not by import — worth knowing before you add a state to one and not the other.
```mermaid
stateDiagram-v2
[*] --> CREATED
CREATED --> SUCCESS: amount is 0, or plan has a balance_holder
CREATED --> PENDING: deposit submitted to wallet
PENDING --> SUCCESS: wallet verify returns state 5
PENDING --> FAILED: wallet verify returns a non-success state
PENDING --> EXPECTED_FAILURE: submit/verify call itself raised
FAILED --> [*]
SUCCESS --> [*]
EXPECTED_FAILURE --> [*]
```
`EXPECTED_FAILURE` means "we don't actually know" — the HTTP call to wallet errored, so promotions guesses failure but the deposit may have gone through. There's no automated reconciliation job for this state today.
---
## API surface
Two front doors into the same engine. User-token endpoints live under `/promotions/`; application-token endpoints live under `/api/v2/promotions/application/<user_uuid>/` for callers acting on behalf of a user they hold no session for.
| Path | View | Auth | Purpose |
|---|---|---|---|
| `POST /promotions/api/v1/events/submit` | `ApplicationEventSubmitAPIView` | token | Fire-and-forget: save the event, queue `analyze_event_task`. |
| `GET /promotions/api/v1/events/<label>/status/` | `ApplicationEventStatusApiView` | token | Has this user already been paid for this event label, and if not, what would they get? |
| `POST /promotions/api/v1/plans/<plan>/` | `ApplicationPromoteUserApiView` | token | Synchronous: evaluate one specific plan right now, no queue. |
| `GET /promotions/api/v1/plans/<plan>/promotins` | `ApplicationPromotionListApiView` | token | List this user's promotion history for a plan. (Route typo — "promotins" — is load-bearing; don't casually rename.) |
| `GET/POST /promotions/api/v2/plans/` | `UserPlanViewSet` | token | Self-service plan browsing (DRF router). |
| `POST .../application/<user_uuid>/event/` | `ApplicationEventViewSet` | app | Same as event submit, but the caller supplies `user_uuid` instead of holding the user's own token; lazily creates the shadow `User` via accounts. |
| `GET .../application/<user_uuid>/event/<label>/status/` | `ApplicationEventViewSet.status` | app | App-side equivalent of the status check above. |
| `GET/POST .../application/<user_uuid>/plan/` | `ApplicationUserPlanViewSet` | app | List plans; `promote` action evaluates one on the user's behalf. |
---
## Playbook: adding a new promotion, end to end
Concrete steps, mirroring the real `first-ad-create` fixture in `apps/promotions/tests.py`. No code changes required for a straightforward percentage-of-nothing flat reward — it's entirely data.
1. **Register the trigger event.** Create an `EventSaver` row naming the event label the calling app will send, and whether it's once-per-user.
```python
EventSaver.objects.create(
event_label="ads::first-ad-create",
title="first-ad-create",
save_once=True, # duplicate submits raise, don't re-pay
)
```
2. **Create the plan.** Fund it with a `balance` and list the event label(s) that should trigger it. `processor` is required by the model but currently has no behavioral effect (see watch list) — `OTHERS` is the safe default.
```python
Plan.objects.create(
title="first-ad-create",
event_list=["ads::first-ad-create"],
balance=30_000_000,
processor=ProcessorTypeChoices.OTHERS,
)
```
3. **Attach a recipient.** The common case: pay the user who fired the event, a flat amount, no percentage math. Set `wallet_destination` to pick which wallet the payout lands in — `user_reward` (the default) pays into the user's cash-like reward wallet; `advertising_transit` funds billboard/ad credit instead (`WALLET_ADVERTISING_TRANSIT`) — see `Recipient.get_wallet_category_uuid()`. A `wallet_uuid` set directly on the `Recipient` always wins over `wallet_destination`.
```python
Recipient.objects.create(
plan=plan,
label="first-ad-create",
recipient_uuid_field="->event:user",
base_amount_field="30000", # literal, or "event:data_key"
wallet_destination=WalletDestinationChoices.ADVERTISING_TRANSIT,
)
```
4. **Have the calling app submit the event.** Either the async application-token route, or the sync per-plan route if the app wants the result inline.
```http
POST /api/v2/promotions/application/<user_uuid>/event/
{ "label": "ads::first-ad-create", "data": {} }
```
5. **Watch it resolve.** Async submits fan out through `analyze_event_task` to every `Plan` whose `event_list` matches, each producing a `Promotion` row you can inspect in admin or via the status endpoint.
---
## Watch list
Know these before you touch the calculation path. Ranked by how badly they'll surprise you, not by file order.
### 🔴 crash — Setting `max_promotion_amount` breaks amount calculation
`apps/promotions/models.py` — `Recipient.get_promotion_amount()`, lines ~276–307
`get_promotion_amount()` returns a single `int` when `max_promotion_amount` is set on `Recipient.data`, but returns a `(base_amount, promotion_amount)` tuple otherwise. Both call sites — `Recipient.promote()` and `Plan.get_configured_promotion_amount()` — unconditionally do `base_amount, promotion_amount = recipient.get_promotion_amount(...)`. The capped branch raises `TypeError: cannot unpack non-iterable int object`.
**Fix:** never set `max_promotion_amount` on a live `Recipient` today. If you need a cap, make `get_promotion_amount` always return the tuple and clamp `promotion_amount` inside it instead of short-circuiting.
### 🟠 correctness + perf — `event_list__icontains` is a substring text match, not JSON containment
`apps/promotions/models.py` — `PlanQuerySet.related_to_event()`, `get_event_status_for_user()`
Both matching paths filter with `event_list__icontains=event.label`. On a JSONField this casts to text and does a case-insensitive `LIKE` — it does not use the `GinIndex` already defined on `event_list`, and it will false-positive match a plan for `"ads::first-ad-create-v2"` when the incoming label is `"ads::first-ad-create"`.
**Fix:** use `event_list__contains=[event.label]` (jsonb `@>`), which is both exact and index-backed.
### ⚪ dead code — the processor/handler abstraction isn't wired up
`apps/promotions/handlers.py`, `models.py Plan.process_event()`
`Plan.process_event()` ignores `self.processor` entirely and always calls `promote_all()`. `PercentageHandler` and `ReferralHandler` in `handlers.py` both `return` before their real logic and reference an undefined `self.balance` — they're unreachable and would error if called.
**Fix:** if a new promotion type needs calculation logic different from the generic percentage-with-cap in `Recipient.get_promotion_amount`, this is the intended seam — but it needs to be actually dispatched from `process_event`, not assumed to work.
### ⚪ duplication — `views_user.py` and `views_application.py` largely repeat each other
`apps/promotions/views_user.py`, `views_application.py`
`ApplicationPromoteUserApiView`, `ApplicationPromotionListApiView`, and `ApplicationEventSubmitAPIView` are near-identical copies across both files. `get_queryset()`/`_resolve_user()` also mutate `self.request.user` as a side effect, which is easy to miss when tracing a bug.
**Fix:** before adding a third variant of any of these, factor the shared body into a mixin — otherwise a fix applied to one silently doesn't apply to its twin.
### ⚪ idempotency — `Promotion.get_or_create` keys on computed amounts, not just identity
`apps/promotions/models.py` — `Recipient.promote()`, lines ~319–326
`get_or_create(promotion_amount=..., user_uuid=..., event=..., plan=..., recipient=..., base_amount=...)` has no `defaults=`; every field is part of the lookup. If the amount calculation for the same (user, event, plan, recipient) ever produces a different number on a retry — a config change mid-flight, or the bug above getting fixed — you get a second `Promotion` row instead of a duplicate-prevented one.
**Fix:** the real duplicate guard is `EventSaver.save_once` one layer up; don't rely on this `get_or_create` for idempotency if you change how amounts are computed.
---
## File map
**apps/promotions — the engine**
| Path | Purpose |
|---|---|
| `models.py` | Every entity above, plus the recipient DSL and the payment state machine. |
| `handlers.py` | Unwired processor/handler scaffolding — see watch list. |
| `views_user.py` / `urls_user.py` | User-token API, mounted at `/promotions/`. |
| `views_application.py` / `urls_application.py` | Application-token API, mounted at `/api/v2/promotions/application/<user_uuid>/`. |
| `serializers.py` | Thin DRF serializers; business logic stays in models.py. |
| `tasks.py` | One Celery task: `analyze_event_task`. |
| `tests.py` | APITestCase flows with wallet calls mocked — the best template for testing a new plan. |
**apps/gooyal_oauth2 & apps/users — borrowed identity**
| Path | Purpose |
|---|---|
| `gooyal_oauth2/validators.py` | Custom introspection against the central accounts service; materializes local Application/AccessToken rows. |
| `gooyal_oauth2/rest_framework.py` | Scope-checking DRF permission classes used across every view. |
| `users/models.py` | Shadow `User` — UUID PK, created on first contact, no local auth. |
**utils/clients — outbound calls to siblings**
| Path | Purpose |
|---|---|
| `wallet_client.py` | Deposit submit/verify, withdraw, wallet balance lookups. |
| `accounts_client.py` | User profile + application detail lookups. |
| `notifications_client.py` | Push + email, both best-effort. |
| `gooyal_*_client/` | Generated openapi-python-client SDKs per README.srt — regenerate, don't hand-edit. |
---
## Config reference
Non-secret values only — pulled from `main/settings.py` and this checkout's own `.env`.
| Variable | Points at |
|---|---|
| `OAUTH2_PROVIDER_PUBLIC_URL` / `_PRIVATE_URL` | Central accounts service's OAuth2 endpoints (token issuance, introspection). Points at the accounts service's staging environment — see this checkout's own `.env` for the actual hostname. |
| `ACCOUNTS_BASE_PUBLIC_URL` | Accounts service REST API (user/application profile reads). |
| `WALLET_BASE_PUBLIC_URL` | Wallet service REST API (deposit submit/verify). |
| `WALLET_RIAL_DEPOSIT` | User-side "real money" wallet type UUID. Not currently read by any code path in this service — kept for parity with the shared naming convention used across the other Gooyal repos that touch the same wallet-service UUIDs (`advertising`, `settlement`, `ipg`). |
| `WALLET_USER_BILLBOARD_VISIT_INCOME` | User-side reward-token wallet type UUID (formerly `WALLET_REWARD`). `payee_wallet` when `Recipient.wallet_destination` is `user_reward` (the default) — see `Recipient.get_wallet_category_uuid()`. |
| `WALLET_PROMOTIONS_CREDIT` | Company-side pool payouts are drawn from — the deposit's `payer_wallet` (formerly hardcoded to the same UUID as the payee side; see [`docs/wallet_refactor.md`](docs/wallet_refactor.md)). Needs a real UUID from the wallet-service team before this service can submit a deposit. |
| `WALLET_ADVERTISING_TRANSIT` | Same wallet-service UUID as advertising's own setting of the same name. `payee_wallet` when `Recipient.wallet_destination` is `advertising_transit` — billboard/ad credit rather than a user reward — see `Recipient.get_wallet_category_uuid()`. |
| `NOTIFICATIONS_BASE_PUBLIC_URL` | Notifications service REST API. |
| `OAUTH2_CLIENT_ID` / `_SECRET` / `_SCOPES` | This service's own client-credentials identity, used for every outbound call above via `login_as_client_credentials()` (token cached under the key `promotions_access_token`). |
---
*Compiled by reading `apps/promotions`, `apps/gooyal_oauth2`, `apps/users`, `utils/clients` in this checkout, cross-referenced against the sibling `wallet` repo and the `.env` files of `chat`/`advertising`/`campaign` under `Winsoo/` to confirm which services are real network boundaries versus copy-pasted boilerplate. The `accounts` and `notifications` services are referenced only through their generated API clients — their source isn't checked out locally.*

View file

@ -1,9 +1,8 @@
import logging import logging
from django.core.exceptions import ImproperlyConfigured
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication
from rest_framework.permissions import ( from rest_framework.permissions import (
IsAuthenticated, BasePermission IsAuthenticated
) )
logger = logging.getLogger("oauth2_provider") logger = logging.getLogger("oauth2_provider")
@ -26,68 +25,3 @@ class IsAuthenticatedOrTokenMatchesOASRequirements(TokenMatchesOASRequirements):
result = (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view) result = (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)
logger.debug(f'authentication result: {result}') logger.debug(f'authentication result: {result}')
return result return result
class TokenMatchesViewSetActions(BasePermission):
"""
:attr:action_required_scopes: dict keyed by view set action name with value: iterable action scope lists
This fulfills the [Open API Specification (OAS; formerly Swagger)](https://www.openapis.org/)
list of alternative Security Requirements Objects for oauth2 or openIdConnect:
When a list of Security Requirement Objects is defined on the Open API object or Operation Object,
only one of Security Requirement Objects in the list needs to be satisfied to authorize the request.
[1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject)
For each method, a list of lists of allowed scopes is tried in order and the first to match succeeds.
@example
required_action_scopes = {
'list': [['read']],
'create': [['create1','scope2'], ['alt-scope3'], ['alt-scope4','alt-scope5']],
}
TODO: DRY: subclass TokenHasScope and iterate over values of required_scope?
"""
def has_permission(self, request, view):
token = request.auth
if not token:
return False
if hasattr(token, "scope"): # OAuth 2
required_action_scopes = self.get_required_action_scopes(request, view)
# TODO: use action map instead to analyze method
action_map = view.action_map
a = view.action
if a in required_action_scopes:
logger.debug(
"Required scopes alternatives to access resource: {0}".format(
required_action_scopes[a]
)
)
for alt in required_action_scopes[a]:
if token.is_valid(alt):
return True
return False
else:
logger.warning("no scope action defined for action {0}".format(a))
return False
assert False, (
"TokenMatchesViewSetActions requires the"
"`oauth2_provider.rest_framework.OAuth2Authentication` authentication "
"class to be used."
)
def get_required_action_scopes(self, request, view):
try:
return getattr(view, "required_action_scopes")
except AttributeError:
raise ImproperlyConfigured(
"TokenMatchesViewSetActions requires the view to"
" define the required_action_scopes attribute"
)

View file

@ -2,27 +2,10 @@ from functools import update_wrapper
from django.contrib import admin from django.contrib import admin
from .models import Promotion, Plan, Event, Recipient, EventSaver, AllowedUser, PromotionTransaction from .models import Promotion, Plan, Event
class PromotionAdmin(admin.ModelAdmin):
list_display = ['user_uuid', 'plan', 'promotion_amount', 'recipient', 'event', 'state', 'created_at']
class PromotionTransactionAdmin(admin.ModelAdmin): admin.site.register(Plan)
list_display = ['user_uuid', 'event_label', 'transaction_type', 'amount', 'state', 'promotion', 'reverses', 'created_at'] admin.site.register(Promotion)
list_filter = ['transaction_type', 'state']
search_fields = ['user_uuid', 'event_label']
class PlanAdmin(admin.ModelAdmin):
list_display = ['uuid', 'title', 'balance']
class AllowedUserAdmin(admin.ModelAdmin):
list_display = ['user', 'recipient', 'created_at']
admin.site.register(Plan, PlanAdmin)
admin.site.register(Promotion, PromotionAdmin)
admin.site.register(Event) admin.site.register(Event)
admin.site.register(EventSaver)
admin.site.register(Recipient)
admin.site.register(AllowedUser, AllowedUserAdmin)
admin.site.register(PromotionTransaction, PromotionTransactionAdmin)

View file

@ -8,30 +8,34 @@ from utils.clients.wallet_client import deposit_to_user_wallet_submit, deposit_t
logger = logging.getLogger("promotion_handler") logger = logging.getLogger("promotion_handler")
class ProcessorTypeChoices(models.TextChoices): class PromotionTypeChoices(models.TextChoices):
PERCENTAGE = 'percentage', _('percentage') PERCENTAGE = 'percentage', _('percentage')
REFERRAL = 'referral', _('referral')
OTHERS = 'others', _('others') OTHERS = 'others', _('others')
class PromotionTypeChoices(models.TextChoices): @enum_property
pass def handler(self):
if self.name == self.PERCENTAGE.name:
return PercentageHandler
elif self.name == self.OTHERS.name:
return OtherHandler
else:
raise NotImplementedError()
class BasePromotionHandler: class BasePromotionHandler:
def __init__(self, plan): def __init__(self, application=None, policy=None, balance=None):
self.plan = plan self.application = application
self.policy = policy
self.balance = balance
def calculate(self, **kwargs): def calculate(self, **kwargs):
# condition and amount
raise NotImplementedError() raise NotImplementedError()
def promote(self, **kwargs): def promote(self, **kwargs):
raise NotImplementedError() raise NotImplementedError()
# def get_recipient_uuid(self, recipient, **kwargs):
# raise NotImplementedError()
class PercentageHandler(BasePromotionHandler): class PercentageHandler(BasePromotionHandler):
def calculate(self, **kwargs): def calculate(self, **kwargs):
@ -44,8 +48,8 @@ class PercentageHandler(BasePromotionHandler):
promotion_amount = 1000 if self.balance >= 1000 else self.balance promotion_amount = 1000 if self.balance >= 1000 else self.balance
return promotion_amount return promotion_amount
def promote(self, **kwargs): def promote(self, promotion, **kwargs):
return pass
payee_type = self.policy.get('payee_type') payee_type = self.policy.get('payee_type')
payee_uuid = self.policy.get('payee_uuid') payee_uuid = self.policy.get('payee_uuid')
@ -85,63 +89,5 @@ class PercentageHandler(BasePromotionHandler):
logger.exception('error in finish visit:') logger.exception('error in finish visit:')
raise e raise e
class ReferralHandler(BasePromotionHandler):
def calculate(self, **kwargs):
event = kwargs.get('event')
plan = self.plan
if base_amount <= 1000 and self.balance >= 1000:
promotion_amount = base_amount
else:
promotion_amount = 1000 if self.balance >= 1000 else self.balance
return promotion_amount
def promote(self, **kwargs):
return
payee_type = self.policy.get('payee_type')
payee_uuid = self.policy.get('payee_uuid')
# promotion_amount = self.calculate(**kwargs)
try:
data = {
"uuid": str(promotion.uuid),
"payee_id": str(payee_uuid),
"payee_type": payee_type,
"amount": promotion.promotion_amount,
"details": {
'description': "for promotion",
'reference_id': str(promotion.uuid),
'application_details_url': ''
},
}
submit_response = deposit_to_user_wallet_submit(data)
try:
submit_response.uuid
except Exception as e:
logger.exception('Failed to submit payment')
raise Exception('Failed to submit payment')
verify_response = deposit_to_user_wallet_verify(str(promotion.uuid))
logger.debug(f'verify response: {verify_response}')
try:
if not verify_response.uuid or verify_response.state != 5:
logger.debug('Failed to verify payment')
raise Exception('Failed to verify payment')
else:
return True
except Exception as e:
logger.exception('Failed to verify payment')
raise Exception('Failed to verify payment')
except Exception as e:
logger.exception('error in finish visit:')
raise e
# def get_recipient_uuid(self, recipient, **kwargs):
# return recipient.uuid
class OtherHandler(BasePromotionHandler): class OtherHandler(BasePromotionHandler):
pass pass

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.7 on 2025-10-30 10:50 # Generated by Django 5.2.4 on 2025-08-02 16:18
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
@ -16,67 +16,18 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='EventSaver',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('title', models.CharField(max_length=255, verbose_name='title')),
('event_label', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='label')),
('save_once', models.BooleanField(default=False, verbose_name='save once')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Event',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('label', models.CharField(db_index=True, max_length=255)),
('data', models.JSONField()),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel( migrations.CreateModel(
name='Plan', name='Plan',
fields=[ fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)), ('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('event_list', models.JSONField(default=list)),
('title', models.CharField(db_index=True, max_length=255, verbose_name='title')), ('title', models.CharField(db_index=True, max_length=255, verbose_name='title')),
('description', models.TextField(blank=True, verbose_name='description')), ('description', models.TextField(blank=True, verbose_name='description')),
('processor', models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')), ('promotion_type', models.CharField(choices=[('wallet percentage', 'Wallet percentage'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')),
('data', models.JSONField(blank=True, default=dict, null=True, verbose_name='data')), ('policy', models.JSONField(blank=True, default=dict, null=True, verbose_name='policy')),
('balance', models.IntegerField(default=0, verbose_name='balance')), ('balance', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='balance')),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Recipient',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('label', models.CharField(db_index=True, max_length=255)),
('wallet_uuid', models.UUIDField(blank=True, null=True)),
('promotion_type', models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type')),
('recipient_uuid_field', models.CharField(db_index=True, max_length=255)),
('base_amount_field', models.CharField(db_index=True, max_length=255)),
('data', models.JSONField(blank=True, default=dict, null=True)),
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='recipients', to='promotions.plan')),
], ],
options={ options={
'abstract': False, 'abstract': False,
@ -88,14 +39,11 @@ class Migration(migrations.Migration):
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)), ('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('user_uuid', models.UUIDField()), ('state', models.IntegerField(choices=[(0, 'Created'), (1, 'Assigned'), (10, 'Expired')], default=0)),
('state', models.IntegerField(choices=[(0, 'Created'), (1, 'Assigned'), (2, 'Promoted'), (10, 'Expired')], default=0)),
('base_amount', models.IntegerField(blank=True, null=True)),
('promotion_amount', models.IntegerField(blank=True, null=True)),
('data', models.JSONField(blank=True, null=True)), ('data', models.JSONField(blank=True, null=True)),
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.plan')), ('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.plan')),
('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.recipient')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'abstract': False, 'abstract': False,

View file

@ -1,29 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-06 08:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='promotion',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='promotions', to='promotions.event'),
),
migrations.AlterField(
model_name='plan',
name='processor',
field=models.CharField(choices=[('percentage', 'percentage'), ('referral', 'referral'), ('others', 'others')], db_index=True, max_length=64, verbose_name='processor'),
),
migrations.AlterField(
model_name='recipient',
name='promotion_type',
field=models.CharField(choices=[], db_index=True, max_length=64, verbose_name='promotion type'),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-08-03 12:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='promotion',
name='promotion_amount',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='promotion',
name='state',
field=models.IntegerField(choices=[(0, 'Created'), (1, 'Assigned'), (2, 'Promoted'), (10, 'Expired')], default=0),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-03 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0002_promotion_promotion_amount_alter_promotion_state'),
]
operations = [
migrations.AlterField(
model_name='plan',
name='balance',
field=models.IntegerField(default=0, verbose_name='balance'),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-08 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0002_promotion_event_alter_plan_processor_and_more'),
]
operations = [
migrations.AlterField(
model_name='recipient',
name='promotion_type',
field=models.CharField(blank=True, choices=[], db_index=True, max_length=64, null=True, verbose_name='promotion type'),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-11 15:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0003_alter_recipient_promotion_type'),
]
operations = [
migrations.AlterField(
model_name='promotion',
name='state',
field=models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure')], default=1),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-03 12:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0003_alter_plan_balance'),
]
operations = [
migrations.AddField(
model_name='promotion',
name='base_amount',
field=models.IntegerField(blank=True, null=True),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 5.2.4 on 2025-11-12 11:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("promotions", "0004_alter_promotion_state"),
]
operations = [
migrations.AlterField(
model_name="event",
name="user",
field=models.UUIDField(blank=True, null=True),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 5.2.6 on 2025-09-16 17:32
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0004_promotion_base_amount'),
]
operations = [
migrations.CreateModel(
name='Event',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('label', models.CharField(db_index=True, max_length=255)),
('details', models.JSONField()),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='plan',
name='event_list',
field=models.JSONField(default=list),
),
]

View file

@ -1,37 +0,0 @@
# Generated by Django 5.1.7 on 2025-11-15 11:19
import django.contrib.postgres.indexes
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('promotions', '0005_alter_event_user'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.AddIndex(
model_name='event',
index=django.contrib.postgres.indexes.GinIndex(fields=['data'], name='promotions_event_data_bc656abc'),
),
migrations.AddIndex(
model_name='plan',
index=django.contrib.postgres.indexes.GinIndex(fields=['event_list'], name='promotions_plan_event_783bb033'),
),
migrations.AddIndex(
model_name='plan',
index=django.contrib.postgres.indexes.GinIndex(fields=['data'], name='promotions_plan_data_b632bc41'),
),
migrations.AddIndex(
model_name='promotion',
index=django.contrib.postgres.indexes.GinIndex(fields=['data'], name='promotions_promotion_cb76da3e'),
),
migrations.AddIndex(
model_name='recipient',
index=django.contrib.postgres.indexes.GinIndex(fields=['data'], name='promotions_recipient_fba22446'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-09-18 11:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('promotions', '0005_event_plan_event_list'),
]
operations = [
migrations.RenameField(
model_name='event',
old_name='details',
new_name='data',
),
]

View file

@ -1,29 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-22 10:12
import django_minio_backend.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0006_event_promotions_event_data_bc656abc_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='event',
options={'ordering': ('-created_at',)},
),
migrations.AddField(
model_name='plan',
name='banner',
field=models.ImageField(blank=True, null=True, storage=django_minio_backend.models.MinioBackend(), upload_to=django_minio_backend.models.iso_date_prefix),
),
migrations.AddField(
model_name='plan',
name='description_details',
field=models.JSONField(blank=True, default={}, verbose_name='description details'),
preserve_default=False,
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.2.6 on 2025-09-20 11:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0006_rename_details_event_data'),
]
operations = [
migrations.AddField(
model_name='plan',
name='max_amount',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='plan',
name='min_amount',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='plan',
name='promotion_type',
field=models.CharField(choices=[('percentage', 'percentage'), ('others', 'others')], db_index=True, max_length=64, verbose_name='promotion type'),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.2.6 on 2025-09-20 12:52
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0007_plan_max_amount_plan_min_amount_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
]
operations = [
migrations.AddField(
model_name='event',
name='application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
),
migrations.AddField(
model_name='event',
name='user',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 5.2.4 on 2026-06-25 06:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0007_alter_event_options_plan_banner_and_more'),
]
operations = [
migrations.AddField(
model_name='plan',
name='balance_holder',
field=models.UUIDField(blank=True, null=True),
),
migrations.AlterField(
model_name='plan',
name='description_details',
field=models.JSONField(blank=True, default=dict, verbose_name='description details'),
),
]

View file

@ -1,40 +0,0 @@
import uuid
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('promotions', '0008_plan_balance_holder_alter_plan_description_details'),
]
operations = [
migrations.AddField(
model_name='recipient',
name='access_type',
field=models.CharField(choices=[('restricted', 'restricted'), ('public', 'public')],
db_index=True, default='public', max_length=64, verbose_name='access type'),
),
migrations.CreateModel(
name='AllowedUser',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True,
serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='allowed_recipients', to=settings.AUTH_USER_MODEL)),
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='allowed_users', to='promotions.recipient')),
],
options={
'constraints': [
models.UniqueConstraint(fields=('user', 'recipient'), name='unique_allowed_user_recipient'),
],
},
),
]

View file

@ -1,19 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0009_recipient_access_type_alloweduser'),
]
operations = [
migrations.AddField(
model_name='recipient',
name='wallet_destination',
field=models.CharField(choices=[('user_reward', 'user reward wallet'),
('advertising_transit', 'advertising transit wallet')],
db_index=True, default='user_reward', max_length=32,
verbose_name='wallet destination'),
),
]

View file

@ -1,43 +0,0 @@
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('promotions', '0010_recipient_wallet_destination'),
]
operations = [
migrations.CreateModel(
name='PromotionTransaction',
fields=[
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
('user_uuid', models.UUIDField(db_index=True)),
('event_label', models.CharField(db_index=True, max_length=255)),
('transaction_type', models.IntegerField(choices=[(1, 'payout'), (2, 'rollback')], db_index=True)),
('holder_wallet', models.UUIDField()),
('destination_wallet', models.UUIDField()),
('amount', models.IntegerField(default=0)),
('state', models.IntegerField(choices=[(1, 'created'), (2, 'delayed'), (3, 'pending'), (4, 'incomplete'), (5, 'success'), (6, 'failed'), (7, 'expected_failure')], db_index=True, default=1)),
('external_uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('promotion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='promotions.promotion')),
('reverses', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reversed_by', to='promotions.promotiontransaction')),
],
options={
'ordering': ['created_at'],
},
),
migrations.AddConstraint(
model_name='promotiontransaction',
constraint=models.UniqueConstraint(fields=('promotion', 'transaction_type'), name='unique_promotion_transaction_type'),
),
migrations.AddConstraint(
model_name='promotiontransaction',
constraint=models.UniqueConstraint(condition=models.Q(('transaction_type', 2)), fields=('user_uuid', 'event_label'), name='unique_promotion_rollback'),
),
]

View file

@ -1,68 +1,23 @@
import uuid
from django.conf import settings from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F, Q from django.db.models import F
from django.utils import timezone from django.utils import timezone
from django_minio_backend import MinioBackend, iso_date_prefix
from oauth2_provider.settings import oauth2_settings from oauth2_provider.settings import oauth2_settings
from apps.promotions import handlers from apps.promotions.handlers import PromotionTypeChoices, BasePromotionHandler
import logging
from apps.promotions.handlers import ProcessorTypeChoices, BasePromotionHandler, PromotionTypeChoices
from apps.users.models import User from apps.users.models import User
from utils.clients.notifications_client import notifications_push_user
from utils.clients.wallet_client import deposit_to_user_wallet_submit, deposit_to_user_wallet_verify
from utils.models import BaseModel from utils.models import BaseModel
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
class EventSaverQuerySet(models.QuerySet):
pass
class EventSaver(BaseModel):
title = models.CharField(_('title'), max_length=255)
event_label = models.CharField(_('label'), max_length=255, unique=True, db_index=True)
save_once = models.BooleanField(_('save once'), default=False)
objects = EventSaverQuerySet.as_manager()
def __str__(self):
return self.title
def save_event(self, **kwargs):
user = kwargs.get('user')
application = kwargs.get('application')
label = kwargs.get('label')
data = kwargs.get('data')
if self.save_once:
event, created = Event.objects.get_or_create(user=user, label=label,
defaults={"data": data, "application": application})
if created:
return event
else:
# TODO: good exception
raise Exception(_('already saved'))
else:
event = Event.objects.create(user=user, application=application, label=label, data=data)
return event
class EventQuerySet(models.QuerySet): class EventQuerySet(models.QuerySet):
pass pass
class Event(BaseModel): class Event(BaseModel):
user = models.UUIDField(null=True, blank=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
related_name='+', null=True, blank=True) related_name='+', null=True, blank=True)
label = models.CharField(max_length=255, db_index=True) label = models.CharField(max_length=255, db_index=True)
@ -70,407 +25,111 @@ class Event(BaseModel):
objects = EventQuerySet.as_manager() objects = EventQuerySet.as_manager()
class Meta:
indexes = [
GinIndex(
fields=['data'],
# hashlib.md5("promotions_event_data_GinIndex".encode()).hexdigest()[:8] used for hash
name='promotions_event_data_bc656abc'
),
]
ordering = ('-created_at',)
def __str__(self): def __str__(self):
return self.label return self.label
def analyze(self): def analyze(self):
for plan in Plan.objects.related_to_event(self): for plan in Plan.objects.get_related_to_event(self):
yield plan.process_event(event=self) print(plan)
plan.process_event(self)
class PlanQuerySet(models.QuerySet): class PlanQuerySet(models.QuerySet):
# # this is for list of discount code for example # this is for list of discount code for example
# def calculate(self, user, application=None): def calculate(self, user, application=None):
# for plan in self.filter(application=application)[:10]: for plan in self.filter(application=application)[:10]:
# if not Promotion.objects.used(user): if not Promotion.objects.used(user):
# plan.calculate(user) plan.calculate(user)
def related_to_event(self, event): def get_related_to_event(self, event):
return self.filter(event_list__icontains=event.label) return self.filter(event_list__icontains=event.label)
class Plan(BaseModel): class Plan(BaseModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) # user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, related_name='+', application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
null=True, blank=True) related_name='+', null=True, blank=True)
event_list = models.JSONField(default=list) event_list = models.JSONField(default=list)
title = models.CharField(max_length=255, verbose_name=_('title'), db_index=True) title = models.CharField(max_length=255, verbose_name=_('title'), db_index=True)
banner = models.ImageField(null=True, blank=True, storage=MinioBackend(), upload_to=iso_date_prefix)
description = models.TextField(verbose_name=_('description'), blank=True) description = models.TextField(verbose_name=_('description'), blank=True)
description_details = models.JSONField(verbose_name=_('description details'), blank=True, default=dict) promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True,
processor = models.CharField(max_length=64, verbose_name=_('processor'), db_index=True, choices=PromotionTypeChoices.choices)
choices=ProcessorTypeChoices.choices) # TODO: processor type min_amount = models.FloatField(null=True, blank=True)
data = models.JSONField(default=dict, verbose_name=_('data'), null=True, blank=True) max_amount = models.FloatField(null=True, blank=True)
policy = models.JSONField(default=dict, verbose_name=_('policy'), null=True, blank=True)
balance_holder = models.UUIDField(null=True, blank=True)
balance = models.IntegerField(verbose_name=_('balance'), default=0) balance = models.IntegerField(verbose_name=_('balance'), default=0)
objects = PlanQuerySet.as_manager() objects = PlanQuerySet.as_manager()
class Meta:
indexes = [
GinIndex(
fields=['event_list'],
# hashlib.md5("promotions_plan_event_list_GinIndex".encode()).hexdigest()[:8] used for hash
name='promotions_plan_event_783bb033'
),
GinIndex(
fields=['data'],
# hashlib.md5("promotions_plan_data_GinIndex".encode()).hexdigest()[:8] used for hash
name='promotions_plan_data_b632bc41'
),
]
def __str__(self): def __str__(self):
return self.title return self.title
def get_configured_promotion_amount(self, user_uuid=None): @property
access_filter = Q(access_type=RecipientTypeChoices.PUBLIC) def handler(self) -> BasePromotionHandler:
if user_uuid: return PromotionTypeChoices(self.promotion_type).handler(
access_filter |= Q(access_type=RecipientTypeChoices.RESTRICTED, allowed_users__user__uuid=user_uuid) application=self.application,
policy=self.policy,
balance=self.balance,
)
recipients = self.recipients.filter( def calculate_promotion(self, **kwargs):
recipient_uuid_field='->event:user' #TODO: instead of hard code use better soloution handler = self.handler
).filter(access_filter).distinct() # self.base_amount = kwargs.get('base_amount', 0)
return handler.calculate(**kwargs)
amount = 0
for recipient in recipients:
base_amount, promotion_amount = recipient.get_promotion_amount()
amount += promotion_amount
return amount
def is_promotion_used_by(self, user_uuid):
return self.promotions.filter(user_uuid=user_uuid).exclude(
state__in=[PaymentStateChoices.FAILED, PaymentStateChoices.EXPECTED_FAILURE]
).exists()
def process_event(self, event=None):
# TODO: processor
return self.promote_all(event=event)
def promote_all(self, event=None):
for recipient in self.recipients.all():
yield recipient.promote(plan=self, event=event)
def reserve_promotion_amount(self, amount): def reserve_promotion_amount(self, amount):
if self.balance_holder:
balance_holder_object = Promotion.objects.get(pk=self.balance_holder)
balance_field_name = 'promotion_amount'
else:
balance_holder_object = self
balance_field_name = 'balance'
updated = False updated = False
if amount and self.balance >= amount:
select_query_kwargs = { updated = Plan.objects.filter(pk=self.pk, balance__gte=amount).update(
"pk": balance_holder_object.pk, balance=F('balance') - amount
f"{balance_field_name}__gte": amount )
}
update_query_kwargs = {
balance_field_name: F(balance_field_name) - amount
}
if amount and getattr(balance_holder_object, balance_field_name) >= amount:
updated = balance_holder_object._meta.model.objects.filter(**select_query_kwargs).update(
**update_query_kwargs)
return updated return updated
def process_event(self, event=None):
class RecipientTypeChoices(models.TextChoices): print(event)
RESTRICTED = 'restricted', _('restricted') referral_code = event.data.get('referral_code')
PUBLIC = 'public', _('public') user_uuid = event.data.get('user_uuid')
promotion_amount = self.calculate_promotion(**self.policy)
if referral_code and promotion_amount and not self.promotions.filter(user_id=referral_code,
class WalletDestinationChoices(models.TextChoices): data__user_uuid=user_uuid).exists():
USER_REWARD = 'user_reward', _('user reward wallet') referral_user, created = User.objects.get_or_create(uuid=referral_code)
ADVERTISING_TRANSIT = 'advertising_transit', _('advertising transit wallet') user = User.objects.get_or_create(uuid=user_uuid)
with transaction.atomic():
if self.reserve_promotion_amount(promotion_amount):
class Recipient(BaseModel): promotion, created = Promotion.objects.get_or_create(plan=self, user=referral_user, defaults={
label = models.CharField(max_length=255, db_index=True) "data": event.data
plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='recipients', null=True, blank=True) })
wallet_uuid = models.UUIDField(null=True, blank=True) if not created:
promotion_type = models.CharField(max_length=64, verbose_name=_('promotion type'), db_index=True, raise Exception('Promotion already reserved')
choices=PromotionTypeChoices.choices, null=True, blank=True)
wallet_destination = models.CharField(max_length=32, verbose_name=_('wallet destination'), db_index=True,
choices=WalletDestinationChoices.choices,
default=WalletDestinationChoices.USER_REWARD)
access_type = models.CharField(max_length=64, verbose_name=_('access type'), db_index=True,
choices=RecipientTypeChoices.choices, default=RecipientTypeChoices.PUBLIC)
recipient_uuid_field = models.CharField(max_length=255, db_index=True)
base_amount_field = models.CharField(max_length=255, db_index=True)
data = models.JSONField(null=True, blank=True, default=dict)
class Meta:
indexes = [
GinIndex(
fields=['data'],
# hashlib.md5("promotions_recipient_data_GinIndex".encode()).hexdigest()[:8] used for hash
name='promotions_recipient_fba22446'
),
]
def __str__(self):
return f"{self.label} --> {self.plan}"
def get_wallet_category_uuid(self):
if self.wallet_uuid:
return self.wallet_uuid
if self.wallet_destination == WalletDestinationChoices.ADVERTISING_TRANSIT:
return settings.WALLET_ADVERTISING_TRANSIT
return settings.WALLET_USER_BILLBOARD_VISIT_INCOME
def is_user_allowed(self, user_uuid):
if self.access_type != RecipientTypeChoices.RESTRICTED:
return True
if not user_uuid:
return False
return self.allowed_users.filter(user__uuid=user_uuid).exists()
def get_recipient_uuid(self, plan=None, event=None):
if ':' in self.recipient_uuid_field and not self.recipient_uuid_field.startswith('QS:'):
filters_key_values, recipient_candidate = self.recipient_uuid_field.split('->')
key_values = filters_key_values and filters_key_values.split('&') or []
for key_value in key_values:
model_name_key, value = key_value.split('=')
model_name, key = model_name_key.split(':')
if model_name == 'event':
obj = event
elif model_name == 'plan':
obj = plan
else: else:
raise NotImplementedError() raise Exception('Promotion already reserved')
promotion.promote()
if key.startswith('data__'):
key = key[len('data__'):]
obj_value = obj.data.get(key)
else:
obj_value = getattr(obj, key)
if obj_value != value:
return None
model_name, key = recipient_candidate.split(':')
if model_name == 'event':
obj = event
elif model_name == 'plan':
obj = plan
else:
raise NotImplementedError()
return obj.data.get(key)
elif ':' in self.recipient_uuid_field and self.recipient_uuid_field.startswith('QS:'):
params, recipient_candidate = self.recipient_uuid_field.split('->')
query_set_flag, model, filters, = params.split(':')
filters_key_values = filters.split('&')
filter_params = {}
for key_value in filters_key_values:
key, value = key_value.split('=')
filter_params[key] = value
if model.lower() == 'event':
model_class = Event
elif model.lower() == 'plan':
model_class = Plan
elif model.lower() == 'promotion':
model_class = Promotion
else:
raise NotImplementedError()
model_object = model_class.objects.filter(**filter_params).last()
if model_object:
try:
uuid.UUID(recipient_candidate)
return recipient_candidate
except:
return model_object.data.get(recipient_candidate)
else:
try:
uuid.UUID(self.recipient_uuid_field)
return self.recipient_uuid_field
except:
pass
def get_promotion_amount(self, plan=None, event=None):
# TODO: get base amount from policy_choice
base_amount = 0
max_promotion_amount = int(self.data.get('max_promotion_amount', 0))
promotion_percentage = int(self.data.get('promotion_percentage', 100))
try:
model_name, key = self.base_amount_field.split(':')
if model_name == 'event':
obj = event
elif model_name == 'plan':
obj = plan
else:
raise NotImplementedError()
base_amount = int(obj.data.get(key))
except Exception as e:
try:
base_amount = int(self.base_amount_field) # todo: to float
except:
logger.exception('can not pars base amount')
raise Exception('can not pars base amount')
promotion_amount = int(base_amount * (promotion_percentage / 100))
if max_promotion_amount:
if promotion_amount > max_promotion_amount:
return max_promotion_amount
else:
return promotion_amount
else:
return base_amount, promotion_amount
# @property
# def handler(self) -> BasePromotionHandler:
# return PromotionTypeChoices(self.promotion_type).handler(self)
def promote(self, plan=None, event=None):
recipient = self.get_recipient_uuid(plan, event)
if not recipient:
return None
if not self.is_user_allowed(recipient):
return None
base_amount, promotion_amount = self.get_promotion_amount(plan=plan, event=event)
promotion, created = Promotion.objects.get_or_create(
promotion_amount=promotion_amount,
user_uuid=recipient,
event=event,
plan=plan,
recipient=self,
base_amount=base_amount,
)
if recipient and promotion_amount and created:
def _pending_rollback():
return PromotionTransaction.objects.filter(
user_uuid=recipient, event_label=event.label,
transaction_type=PromotionTransaction.TypeChoices.ROLLBACK,
).exclude(state=PaymentStateChoices.SUCCESS).first()
pending_rollback = _pending_rollback()
if pending_rollback is not None:
# The advertising service already asked to reverse this promotion
# before it was even paid -- don't move the money at all.
promotion.change_state(from_states=[promotion.state],
to_state=PaymentStateChoices.SUCCESS, same_ok=True)
PromotionTransaction.objects.filter(pk=pending_rollback.pk).update(
promotion=promotion, amount=0, state=PaymentStateChoices.SUCCESS,
updated_at=timezone.now())
promotion.refresh_from_db()
return promotion
try:
with transaction.atomic():
reserved = self.plan.reserve_promotion_amount(promotion_amount)
if reserved:
promotion.promote()
else:
raise Exception('promotion reward not reserved')
except Exception as e:
promotion.change_state(from_states=[promotion.state], to_state=PaymentStateChoices.EXPECTED_FAILURE,
same_ok=True)
promotion.refresh_from_db()
# A rollback request may have arrived while the payout above was in
# flight (so the pre-check missed it). Reverse it now that it has settled.
if _pending_rollback() is not None:
rollback_promotion_payout(recipient, event.label)
return promotion
return None
class AllowedUser(BaseModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='allowed_recipients')
recipient = models.ForeignKey(Recipient, on_delete=models.CASCADE, related_name='allowed_users')
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'recipient'], name='unique_allowed_user_recipient')
]
def __str__(self):
return f"{self.user} -> {self.recipient}"
class PaymentStateChoices(models.IntegerChoices):
CREATED = 1, _('created')
DELAYED = 2, _('delayed') # delayed as user wish
PENDING = 3, _('pending') # wait for external service response (or action)
INCOMPLETE = 4, _('incomplete') # started and wait for internal progress to complete
SUCCESS = 5, _('success')
FAILED = 6, _('failed')
EXPECTED_FAILURE = 7, _('expected_failure') # no exact data available but guessed to be failed
class PromotionQuerySet(models.QuerySet): class PromotionQuerySet(models.QuerySet):
def used(self, user): def used(self, user):
return self.filter(user=user, state__in=[Promotion.PaymentStateChoices.CREATED]) return self.filter(user=user, state__in=[Promotion.StateChoices.CREATED])
def is_processed_for_user(self, user_uuid, event_label):
return self.filter(user_uuid=user_uuid, event__label=event_label).exists()
def get_event_status_for_user(user_uuid, event_label):
processed = Promotion.objects.is_processed_for_user(user_uuid, event_label)
promotion_amount = 0
if not processed:
plan = Plan.objects.filter(event_list__icontains=event_label).first()
if plan:
promotion_amount = plan.get_configured_promotion_amount(user_uuid=user_uuid)
return {
'event_label': event_label,
'processed': processed,
'promotion_amount': promotion_amount,
}
class Promotion(BaseModel): class Promotion(BaseModel):
user_uuid = models.UUIDField() # user_uuid class StateChoices(models.IntegerChoices):
CREATED = 0, _('Created')
ASSIGNED = 1, _('Assigned')
PROMOTED = 2, _('Promoted')
EXPIRED = 10, _('Expired')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
related_name='+', null=True, blank=True) related_name='+', null=True, blank=True)
plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True) plan = models.ForeignKey(Plan, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True)
event = models.ForeignKey(Event, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True) state = models.IntegerField(choices=StateChoices.choices, default=StateChoices.CREATED)
recipient = models.ForeignKey(Recipient, on_delete=models.PROTECT, related_name='promotions', null=True, blank=True)
state = models.IntegerField(choices=PaymentStateChoices.choices, default=PaymentStateChoices.CREATED)
base_amount = models.IntegerField(null=True, blank=True) base_amount = models.IntegerField(null=True, blank=True)
promotion_amount = models.IntegerField(null=True, blank=True) promotion_amount = models.IntegerField(null=True, blank=True)
data = models.JSONField(null=True, blank=True) data = models.JSONField(null=True, blank=True)
objects = PromotionQuerySet.as_manager() objects = PromotionQuerySet.as_manager()
class Meta:
indexes = [
GinIndex(
fields=['data'],
# hashlib.md5("promotions_promotion_data_GinIndex".encode()).hexdigest()[:8] used for hash
name='promotions_promotion_cb76da3e'
),
]
def change_state(self, from_states: list | tuple, to_state, same_ok=True, raise_exception=False, def change_state(self, from_states: list | tuple, to_state, same_ok=True, raise_exception=False,
error_message=None, error_message=None,
data: None | dict = None): data: None | dict = None):
@ -493,284 +152,11 @@ class Promotion(BaseModel):
raise Exception(error_message) raise Exception(error_message)
def promote(self, **kwargs): def promote(self, **kwargs):
plan_data = kwargs.get('plan_data', {}) self.change_state(from_states=[Promotion.StateChoices.CREATED], to_state=Promotion.StateChoices.ASSIGNED,
plan_data['updated_at'] = timezone.now() same_ok=False, raise_exception=True)
# plan_data = {"balance": 0}
logger.debug(f"start promotions: {self}") handler = self.plan.handler
if handler.promote(promotion=self, **kwargs):
if self.state not in [PaymentStateChoices.CREATED]: self.change_state(from_states=[Promotion.StateChoices.ASSIGNED], to_state=Promotion.StateChoices.PROMOTED)
raise Exception(_('Cannot promote. not in correct state'))
if not self.promotion_amount:
to_not_payed = self.change_state(
from_states=[PaymentStateChoices.CREATED],
to_state=PaymentStateChoices.SUCCESS,
same_ok=False,
raise_exception=True,
error_message='Failed to update state',
)
return bool(to_not_payed)
to_pending = self.change_state(
from_states=[self.state],
to_state=PaymentStateChoices.PENDING,
same_ok=False,
raise_exception=True,
error_message='Cannot promote. not in current state'
)
# The wallet transfer itself is recorded and driven by a PromotionTransaction
# ledger row (mirrors advertising's AdPayment / escrow's EscrowWalletPayment),
# so every promotion payout has an auditable record and can be reversed.
payout, _created = PromotionTransaction.objects.get_or_create(
promotion=self,
transaction_type=PromotionTransaction.TypeChoices.PAYOUT,
defaults=dict(
user_uuid=self.user_uuid,
event_label=self.event.label if self.event_id else '',
holder_wallet=settings.WALLET_PROMOTIONS_CREDIT,
destination_wallet=self.recipient.get_wallet_category_uuid(),
amount=self.promotion_amount,
),
)
try:
transferred = payout.execute()
except Exception:
self.change_state(
from_states=[self.state],
to_state=PaymentStateChoices.EXPECTED_FAILURE,
same_ok=True,
raise_exception=True,
error_message='Failed to update state',
)
raise Exception('Failed to pay promotion')
if not transferred:
self.change_state(
from_states=[self.state],
to_state=PaymentStateChoices.FAILED,
same_ok=True,
raise_exception=True,
error_message='Failed to update state payment',
)
return False
try:
notifications_push_user(self.user_uuid, self.recipient.label, self.recipient.label, 5, extras={})
except:
# TODO: log
pass
with transaction.atomic():
# updated = Plan.objects.filter(pk=self.plan.pk).update(balance=F('balance') - self.promotion_amount, **plan_data)
updated = Plan.objects.filter(pk=self.plan.pk).update(**plan_data)
if updated:
to_pay_success = self.change_state(
from_states=[self.state],
to_state=PaymentStateChoices.SUCCESS,
same_ok=False,
raise_exception=True,
error_message='Failed to update state refund',
)
return bool(to_pay_success)
else:
# TODO: is it possible?
to_pay_failed = self.change_state(
from_states=[self.state],
to_state=PaymentStateChoices.EXPECTED_FAILURE,
same_ok=False,
raise_exception=True,
error_message='Failed to update state payment',
)
return False
def is_rollbackable(self):
"""A promotion payout can only be reversed if it actually landed in the
advertising transit wallet. A payout made straight to the user's income
wallet is the user's money and is never clawed back."""
return bool(
self.recipient_id
and self.recipient.wallet_destination == WalletDestinationChoices.ADVERTISING_TRANSIT
)
def is_rolled_back(self):
return self.transactions.filter(
transaction_type=PromotionTransaction.TypeChoices.ROLLBACK,
state=PaymentStateChoices.SUCCESS,
).exists()
IN_FLIGHT_PAYMENT_STATES = (
PaymentStateChoices.CREATED,
PaymentStateChoices.PENDING,
PaymentStateChoices.DELAYED,
PaymentStateChoices.INCOMPLETE,
)
class PromotionTransaction(BaseModel):
"""Ledger of every wallet transfer the promotions service makes -- the
payout for a promotion, and any later reversal of it. Mirrors advertising's
``AdPayment`` and escrow's ``EscrowWalletPayment``: each row drives one
external wallet call through its own CREATED -> PENDING -> SUCCESS/FAILED
state, so the money movement is auditable and reversible.
``user_uuid`` / ``event_label`` are denormalised onto the row so a ROLLBACK
can be recorded before the async payout task has even created its
``Promotion`` (see :func:`rollback_promotion_payout` and
``Recipient.promote``).
"""
class TypeChoices(models.IntegerChoices):
PAYOUT = 1, _('payout') # promotions credit -> recipient wallet (transit or user)
ROLLBACK = 2, _('rollback') # advertising transit -> promotions credit
promotion = models.ForeignKey(Promotion, on_delete=models.SET_NULL, null=True, blank=True,
related_name='transactions')
user_uuid = models.UUIDField(db_index=True)
event_label = models.CharField(max_length=255, db_index=True)
transaction_type = models.IntegerField(choices=TypeChoices.choices, db_index=True)
holder_wallet = models.UUIDField() # 1st arg to deposit_to_user_wallet_submit
destination_wallet = models.UUIDField() # data['payee_wallet']
amount = models.IntegerField(default=0)
state = models.IntegerField(choices=PaymentStateChoices.choices,
default=PaymentStateChoices.CREATED, db_index=True)
external_uuid = models.UUIDField(default=uuid.uuid4, editable=False) # idempotency key to the wallet service
reverses = models.ForeignKey('self', on_delete=models.PROTECT, null=True, blank=True,
related_name='reversed_by')
class Meta:
ordering = ['created_at']
constraints = [
models.UniqueConstraint(fields=['promotion', 'transaction_type'],
name='unique_promotion_transaction_type'),
models.UniqueConstraint(fields=['user_uuid', 'event_label'],
condition=models.Q(transaction_type=2),
name='unique_promotion_rollback'),
]
def __str__(self):
return f"{self.get_transaction_type_display()} {self.amount} ({self.event_label})"
def _set_state(self, state):
PromotionTransaction.objects.filter(pk=self.pk).update(state=state, updated_at=timezone.now())
self.state = state
def execute(self):
"""Run the wallet transfer once. Returns True only on a confirmed
transfer; leaves the row FAILED / EXPECTED_FAILURE (and re-raises) on
error, same as the promote()/AdPayment style. No automatic retry."""
if self.state == PaymentStateChoices.SUCCESS:
return True return True
if self.state != PaymentStateChoices.CREATED: return False
return False
if not self.amount:
self._set_state(PaymentStateChoices.SUCCESS)
return True
self._set_state(PaymentStateChoices.PENDING)
data = {
"uuid": str(self.external_uuid),
"payee": str(self.user_uuid),
"payee_type": 1,
"payee_wallet": str(self.destination_wallet),
"amount": self.amount,
"details": {
'description': str(self.get_transaction_type_display()),
'reference_id': str(self.pk),
'application_details_url': '',
},
}
try:
submit_response = deposit_to_user_wallet_submit(str(self.holder_wallet), data)
if not getattr(submit_response, 'uuid', None):
raise Exception('wallet submit returned no uuid')
except Exception:
self._set_state(PaymentStateChoices.FAILED)
raise
try:
verify_response = deposit_to_user_wallet_verify(str(self.holder_wallet), submit_response.uuid)
if not getattr(verify_response, 'uuid', None):
raise Exception('wallet verify returned no uuid')
except Exception:
self._set_state(PaymentStateChoices.EXPECTED_FAILURE)
raise
if verify_response.state != 5:
self._set_state(PaymentStateChoices.FAILED)
return False
self._set_state(PaymentStateChoices.SUCCESS)
return True
def rollback_promotion_payout(user_uuid, event_label):
"""Reverse a promotion payout for ``(user_uuid, event_label)`` back out of
the advertising transit wallet into the promotions credit wallet.
The promotion stays consumed -- the user cannot earn it again -- only the
money moves, and only if the payout actually landed in the transit wallet.
Idempotent. Returns ``(status, amount)`` where status is one of
``'reversed'`` (money moved), ``'deferred'`` (payout not processed yet --
``Recipient.promote()`` will suppress it), ``'nothing'`` (nothing to
reverse: zero payout, or paid straight to the user's wallet).
"""
with transaction.atomic():
existing = PromotionTransaction.objects.select_for_update().filter(
user_uuid=user_uuid, event_label=event_label,
transaction_type=PromotionTransaction.TypeChoices.ROLLBACK,
).first()
if existing is not None and existing.state == PaymentStateChoices.SUCCESS:
return ('reversed' if existing.amount else 'nothing', existing.amount)
payout = PromotionTransaction.objects.select_for_update().filter(
user_uuid=user_uuid, event_label=event_label,
transaction_type=PromotionTransaction.TypeChoices.PAYOUT,
).first()
payout_in_flight = payout is not None and payout.state in IN_FLIGHT_PAYMENT_STATES
promotion_settled_without_payout = payout is None and Promotion.objects.filter(
user_uuid=user_uuid, event__label=event_label, state=PaymentStateChoices.SUCCESS,
).exists()
if payout is not None and payout.state == PaymentStateChoices.SUCCESS \
and payout.promotion_id and payout.promotion.is_rollbackable() and payout.amount:
amount = payout.amount
else:
amount = 0
promo = payout.promotion if payout is not None else None
rollback, created = PromotionTransaction.objects.get_or_create(
user_uuid=user_uuid, event_label=event_label,
transaction_type=PromotionTransaction.TypeChoices.ROLLBACK,
defaults=dict(
promotion=promo,
holder_wallet=settings.WALLET_ADVERTISING_TRANSIT,
destination_wallet=settings.WALLET_PROMOTIONS_CREDIT,
amount=amount,
reverses=payout,
),
)
if not created and rollback.state != PaymentStateChoices.SUCCESS:
# refresh the amount/links now that the payout may have appeared
PromotionTransaction.objects.filter(pk=rollback.pk).exclude(
state=PaymentStateChoices.SUCCESS).update(
promotion=promo, amount=amount, reverses=payout, updated_at=timezone.now())
rollback.amount = amount
if payout is None and not promotion_settled_without_payout:
return ('deferred', 0) # honoured later by Recipient.promote()
if payout_in_flight:
return ('deferred', 0)
if amount == 0:
rollback._set_state(PaymentStateChoices.SUCCESS)
return ('nothing', 0)
rollback.execute() # raises on wallet failure -> caller aborts + retries
return ('reversed', amount)

View file

@ -1,6 +1,22 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Promotion, Plan, Event, Recipient from .models import Promotion, Plan, Event
class PromotionSerializer(serializers.ModelSerializer):
base_amount = serializers.IntegerField(required=True)
promotion_amount = serializers.IntegerField(read_only=True)
class Meta:
model = Promotion
fields = (
"base_amount",
"promotion_amount",
"plan",
"user",
"application",
)
read_only_fields = ('user','application', "promotion_amount",)
class EventSerializer(serializers.ModelSerializer): class EventSerializer(serializers.ModelSerializer):
@ -12,25 +28,7 @@ class EventSerializer(serializers.ModelSerializer):
"user", "user",
"application", "application",
) )
read_only_fields = ('user', 'application') read_only_fields = ('user','application')
class PromotionSerializer(serializers.ModelSerializer):
# base_amount = serializers.IntegerField(required=True)
# promotion_amount = serializers.IntegerField(read_only=True)
event = EventSerializer(read_only=True)
user = serializers.UUIDField(source='user_uuid')
class Meta:
model = Promotion
fields = (
"uuid",
"event",
"promotion_amount",
"state",
"user",
)
class PlanSerializer(serializers.ModelSerializer): class PlanSerializer(serializers.ModelSerializer):
base_amount = serializers.IntegerField(required=True) base_amount = serializers.IntegerField(required=True)
@ -38,12 +36,13 @@ class PlanSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Plan model = Plan
fields = ("title", fields = (
"description", "title",
"base_amount", "description",
"application", "base_amount",
"promotion_amount" "application",
) "promotion_amount"
)
read_only_fields = ('application', "promotion_amount", "title", "description") read_only_fields = ('application', "promotion_amount", "title", "description")
# def get_promotion_amount(self, obj): # def get_promotion_amount(self, obj):
@ -58,59 +57,5 @@ class PlanPromotSerializer(serializers.ModelSerializer):
"user", "user",
"application", "application",
) )
read_only_fields = ('user', 'application') read_only_fields = ('user','application')
class PromoteSerializer(serializers.ModelSerializer):
label = serializers.CharField(write_only=True)
data = serializers.JSONField(write_only=True)
promotions = PromotionSerializer(many=True, read_only=True)
class Meta:
model = Promotion
fields = (
"promotions",
"label",
"data",
)
class UserRecipientSerializer(serializers.ModelSerializer):
class Meta:
model = Recipient
fields = ['label',
"base_amount_field"]
class PromotionStatusSerializer(serializers.Serializer):
event_label = serializers.CharField(read_only=True)
processed = serializers.BooleanField(read_only=True)
promotion_amount = serializers.IntegerField(read_only=True, allow_null=True)
class PromotionRollbackSerializer(serializers.Serializer):
event_label = serializers.CharField(read_only=True)
# reversed -> money moved back out of the advertising transit wallet
# deferred -> payout not settled yet; it will be suppressed when it runs
# nothing -> nothing to reverse (already rolled back, or paid to the user)
status = serializers.ChoiceField(read_only=True, choices=['reversed', 'deferred', 'nothing'])
amount = serializers.IntegerField(read_only=True)
class UserPlanSerializer(serializers.ModelSerializer):
recipients = UserRecipientSerializer(many=True, read_only=True)
class Meta:
model = Plan
fields = ("title",
'banner',
"description",
"description_details",
"recipients",
)
read_only_fields = ("title", "description", "description_details", 'banner', "recipients")
# def get_promotion_amount(self, obj):
# user = self.context['request'].user
# return obj.calculate_promotion(user)

View file

@ -1,17 +1,8 @@
import logging
from apps.promotions.models import Event from apps.promotions.models import Event
from main import celery_app from main import celery_app
logger = logging.getLogger(__name__)
@celery_app.task @celery_app.task
def analyze_event_task(event_uuid): def analyze_event_task(event_uuid):
event = Event.objects.get(uuid=event_uuid) event = Event.objects.get(uuid=event_uuid)
for plan_analyze_result in event.analyze(): event.analyze()
try:
for promotion in plan_analyze_result:
print(promotion)
except Exception:
logger.exception('Failed to process plan for event %s', event_uuid)

View file

@ -1,900 +1,3 @@
import uuid from django.test import TestCase
from datetime import timedelta
from unittest.mock import patch
from django.urls import reverse # Create your tests here.
from django.utils import timezone
from oauth2_provider.models import get_access_token_model, get_application_model
from rest_framework.test import APITestCase, override_settings, APIClient
from apps.promotions.tasks import analyze_event_task
from apps.users.models import User
from apps.promotions.models import Plan, Promotion, EventSaver, ProcessorTypeChoices, Event, Recipient, \
PaymentStateChoices, WalletDestinationChoices
AccessToken = get_access_token_model()
Application = get_application_model()
application_uuid_hex = 'b14e8b86-8f4a-44d9-b29d-badceb470053'
def mock_submit_deposit_success(wallet_uuid, data):
import uuid as sys_uuid
class Tmp():
uuid = sys_uuid.uuid4()
data = Tmp()
return data
def mock_verify_deposit_success(wallet_uuid, transaction_uuid):
import uuid as sys_uuid
class Tmp():
uuid = sys_uuid.uuid4()
state = 5
data = Tmp()
return data
def mock_submit_withdraw_success(data):
import uuid as sys_uuid
class Tmp():
uuid = sys_uuid.uuid4()
data = Tmp()
return data
def mock_verify_withdraw_success(uuid):
import uuid as sys_uuid
class Tmp():
uuid = sys_uuid.uuid4()
state = 5
data = Tmp()
return data
class ApplicationApiFlowsTests(APITestCase):
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
application_uuid = uuid.UUID(application_uuid_hex)
application_access_token = '14naVsdKCbKNOhnElPyXcrwSnqqFbm'
user_access_token = '24naVsdKCbKNOhnElPyXcrwSnqqFbm'
user_1_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
user_2_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb470052')
first_ad_view_reward_label = 'first_ad_view_reward_label'
referral_reward_label = 'referral_reward_label'
settlement_reward_label = 'settlement_reward_label'
serial_reward_label = 'serial_reward_label'
scopes = [
'promotions.application.event:submit',
'promotions.user.self-plans:list-retrieve',
'promotions.application.user-promotions:promote',
'promotions.application.user-promotions:list-retrieve',
'promotions.application.user-plan:promote',
'promotions.application.user-plan:list-retrieve'
]
def setUp(self):
self.submit_deposit_success_patcher = patch('apps.promotions.models.deposit_to_user_wallet_submit',
mock_submit_deposit_success)
self.verify_deposit_success_patcher = patch('apps.promotions.models.deposit_to_user_wallet_verify',
mock_verify_deposit_success)
# self.submit_withdraw_success_patcher = patch('apps.promotions.models.withdraw_from_user_wallet_submit',
# mock_submit_withdraw_success)
# self.verify_withdraw_success_patcher = patch('apps.promotions.models.withdraw_from_user_wallet_verify',
# mock_verify_withdraw_success)
self.submit_deposit_success_patcher.start()
self.verify_deposit_success_patcher.start()
# self.submit_withdraw_success_patcher.start()
# self.verify_withdraw_success_patcher.start()
self.user_1, _ = User.objects.get_or_create(pk=self.user_1_uuid)
self.user_2, _ = User.objects.get_or_create(pk=self.user_2_uuid)
expire_datetime = timezone.now() + timedelta(seconds=3600)
expire_datetime.isoformat()
self.application_access_token, _created = AccessToken.objects.update_or_create(
token=self.application_access_token,
defaults={
"client_id": self.client_id,
# "client_owner": owner,
"application_id": self.application_uuid,
"scope": ' '.join(self.scopes),
"expires": expire_datetime.isoformat(),
},
)
self.user_access_token, _created = AccessToken.objects.update_or_create(
token=self.user_access_token,
defaults={
"user": self.user_1,
"client_id": self.client_id,
# "client_owner": owner,
"application_id": self.application_uuid,
"scope": ' '.join(self.scopes),
"expires": expire_datetime.isoformat(),
},
)
self.application, _created = Application.objects.get_or_create(
client_id=self.client_id,
uuid=self.application_uuid,
)
def tearDown(self):
super().tearDown()
def _create_authorization_header(self, token):
return "Bearer {0}".format(token)
def test_first_ad_view_reward_success(self):
self.first_ad_view_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.first_ad_view_reward_label,
event_label=self.first_ad_view_reward_label,
save_once=True,
)
self.first_ad_view_reward_plan, _created = Plan.objects.get_or_create(
title=self.first_ad_view_reward_label,
balance=10000,
user=self.user_1,
application=self.application,
event_list=[self.first_ad_view_reward_label]
)
self.first_ad_view_reward_recipient, _created = Recipient.objects.get_or_create(
label=self.first_ad_view_reward_label,
plan=self.first_ad_view_reward_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field="->event:user",
base_amount_field="event:base_amount",
)
auth = self._create_authorization_header(self.user_access_token.token)
event_create_data = {
'label': self.first_ad_view_reward_label,
"data": {
'user': self.user_1_uuid,
'base_amount': 800
}
}
response = self.client.get(
reverse('promotions:promotion-list', kwargs={"plan": self.first_ad_view_reward_plan.uuid}),
event_create_data, HTTP_AUTHORIZATION=auth,
format='json')
print(response.json())
print(response.status_code)
# response = self.client.post(reverse('promotions:user-promote', kwargs={"pk":self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json')
response = self.client.post(
reverse('promotions:promotion-create', kwargs={"plan": self.first_ad_view_reward_plan.uuid}),
event_create_data, HTTP_AUTHORIZATION=auth,
format='json')
print(response.json())
self.first_ad_view_reward_plan.refresh_from_db()
self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800)
response = self.client.get(
reverse('promotions:promotion-list', kwargs={"plan": self.first_ad_view_reward_plan.uuid}),
event_create_data, HTTP_AUTHORIZATION=auth,
format='json')
print(response.json())
# response = self.client.post(reverse('promotions:promotion-create', kwargs={"plan": self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json')
# print(response.json())
def test_referral_reward_success(self):
self.referral_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.referral_reward_label,
event_label=self.referral_reward_label,
save_once=True,
)
self.referral_reward_plan, _created = Plan.objects.get_or_create(
title=self.referral_reward_label,
balance=10000,
user=self.user_1,
application=self.application,
event_list=[self.referral_reward_label]
)
self.referral_reward_recipient, _created = Recipient.objects.get_or_create(
label=self.referral_reward_label,
plan=self.referral_reward_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field="->event:referral",
base_amount_field="1000",
)
auth = self._create_authorization_header(self.user_access_token.token)
event_create_data = {
'label': self.referral_reward_label,
"data": {
'user': self.user_1_uuid,
'referral': self.user_2_uuid
}
}
response = self.client.post(
reverse('promotions:promotion-create', kwargs={"plan": self.referral_reward_plan.uuid}), event_create_data,
HTTP_AUTHORIZATION=auth,
format='json')
print(response.json())
self.referral_reward_plan.refresh_from_db()
self.assertEqual(self.referral_reward_plan.balance, 10000 - 1000)
self.assertEqual(response.json()['promotions'][0]['user'], str(self.user_2_uuid))
def test_referral_reward_submit_settlement_success(self):
self.referral_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.referral_reward_label,
event_label=self.referral_reward_label,
save_once=True,
)
self.settlement_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.settlement_reward_label,
event_label=self.settlement_reward_label,
save_once=False,
)
self.settlement_reward_plan, _created = Plan.objects.get_or_create(
title=self.settlement_reward_label,
balance=100000,
user=self.user_1,
application=self.application,
event_list=[self.settlement_reward_label]
)
self.settlement_referral_reward_recipient, _created = Recipient.objects.get_or_create(
label='settlement_referral_reward_label',
plan=self.settlement_reward_plan,
wallet_uuid=self.user_1_uuid,
# recipient_uuid_field=self.user_2_uuid,
recipient_uuid_field=f"QS:Event:label={self.referral_reward_label}&data__referral={self.user_2_uuid}->{self.user_2_uuid}",
base_amount_field="event:base_amount",
)
auth = self._create_authorization_header(self.user_access_token.token)
submit_referral_create_data = {
'label': self.referral_reward_label,
"data": {
'user': self.user_1_uuid,
'referral': self.user_2_uuid
}
}
submit_settlement_create_data = {
'label': self.settlement_reward_label,
"data": {
'user': self.user_1_uuid,
'settlement': self.user_2_uuid,
'base_amount': 7000
}
}
self.assertEqual(Event.objects.count(), 0)
response = self.client.post(reverse('promotions:event-create'), submit_referral_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
self.settlement_reward_plan.refresh_from_db()
self.assertEqual(self.settlement_reward_plan.balance, 100000)
self.assertEqual(Event.objects.count(), 1)
response = self.client.post(
reverse('promotions:promotion-create', kwargs={'plan': str(self.settlement_reward_plan.uuid)}),
submit_settlement_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
response = self.client.post(
reverse('promotions:promotion-create', kwargs={'plan': str(self.settlement_reward_plan.uuid)}),
submit_settlement_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
def test_referral_percentage_reward_submit_settlement_success(self):
self.referral_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.referral_reward_label,
event_label=self.referral_reward_label,
save_once=True,
)
self.settlement_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.settlement_reward_label,
event_label=self.settlement_reward_label,
save_once=False,
)
self.settlement_reward_plan, _created = Plan.objects.get_or_create(
title=self.settlement_reward_label,
balance=100000,
user=self.user_1,
application=self.application,
event_list=[self.settlement_reward_label]
)
self.settlement_referral_reward_recipient, _created = Recipient.objects.get_or_create(
label='settlement_referral_reward_label',
plan=self.settlement_reward_plan,
wallet_uuid=self.user_1_uuid,
# recipient_uuid_field=self.user_2_uuid,
recipient_uuid_field=f"QS:Event:label={self.referral_reward_label}&data__referral={self.user_2_uuid}->{self.user_2_uuid}",
base_amount_field="event:base_amount",
data={"max_promotion_amount": 0, "promotion_percentage": 50}
)
auth = self._create_authorization_header(self.user_access_token.token)
submit_referral_create_data = {
'label': self.referral_reward_label,
"data": {
'user': self.user_1_uuid,
'referral': self.user_2_uuid
}
}
submit_settlement_create_data = {
'label': self.settlement_reward_label,
"data": {
'user': self.user_1_uuid,
'settlement': self.user_2_uuid,
'base_amount': 7000
}
}
self.assertEqual(Event.objects.count(), 0)
response = self.client.post(reverse('promotions:event-create'), submit_referral_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
self.settlement_reward_plan.refresh_from_db()
self.assertEqual(self.settlement_reward_plan.balance, 100000)
self.assertEqual(Event.objects.count(), 1)
response = self.client.post(
reverse('promotions:promotion-create', kwargs={'plan': str(self.settlement_reward_plan.uuid)}),
submit_settlement_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
response = self.client.post(
reverse('promotions:promotion-create', kwargs={'plan': str(self.settlement_reward_plan.uuid)}),
submit_settlement_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
def test_serial_ad_view_promotion_success(self):
serial_id = 'b14e8b86-8f4a-44d9-b29d-badceb470053'
ad_1 = 'b14e8b86-8f4a-44d9-b29d-badceb470054'
ad_2 = 'b14e8b86-8f4a-44d9-b29d-badceb470055'
ad_3 = 'b14e8b86-8f4a-44d9-b29d-badceb470056'
self.serial_ad_view_1_event_saver, _created = EventSaver.objects.get_or_create(
title='serial_ad_view_1',
event_label='serial_ad_view_1',
save_once=True,
)
self.serial_ad_view_2_event_saver, _created = EventSaver.objects.get_or_create(
title='serial_ad_view_2',
event_label='serial_ad_view_2',
save_once=True,
)
self.serial_ad_view_3_event_saver, _created = EventSaver.objects.get_or_create(
title='serial_ad_view_3',
event_label='serial_ad_view_3',
save_once=True,
)
self.serial_promotion_plan, _created = Plan.objects.get_or_create(
title=self.serial_reward_label,
balance=100000,
user=self.user_1,
application=self.application,
event_list=['serial_ad_view_1', 'serial_ad_view_2', 'serial_ad_view_3']
)
self.serial_ad_view_1_recipient, _created = Recipient.objects.get_or_create(
label='serial_ad_view_1_event_saver_label',
plan=self.serial_promotion_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field=f"event:label=serial_ad_view_1&event:data__serial={serial_id}->event:user",
base_amount_field=300,
# data={"max_promotion_amount": 0, "promotion_percentage": 50}
)
self.serial_ad_view_2_recipient, _created = Recipient.objects.get_or_create(
label='serial_ad_view_2_event_saver_label',
plan=self.serial_promotion_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field=f"event:label=serial_ad_view_2&event:data__serial={serial_id}->event:user",
# recipient_uuid_field=f"QS:Event:label=serial_ad_view_2&data__user={self.user_1_uuid}&data__serial={serial_id}->user",
base_amount_field=200,
# data={"max_promotion_amount": 0, "promotion_percentage": 50}
)
self.serial_ad_view_3_recipient, _created = Recipient.objects.get_or_create(
label='serial_ad_view_3_event_saver_label',
plan=self.serial_promotion_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field=f"event:label=serial_ad_view_3&event:data__serial={serial_id}->event:user",
# recipient_uuid_field=f"QS:Event:label=serial_ad_view_3&data__user={self.user_1_uuid}&data__serial={serial_id}->user",
base_amount_field=100,
# data={"max_promotion_amount": 0, "promotion_percentage": 50}
)
auth = self._create_authorization_header(self.user_access_token.token)
serial_ad_1_event_create_data = {
'label': 'serial_ad_view_1',
"data": {
'user': self.user_1_uuid,
'serial': serial_id,
'ad': ad_1
}
}
serial_ad_2_event_create_data = {
'label': 'serial_ad_view_2',
"data": {
'user': self.user_1_uuid,
'serial': serial_id,
'ad': ad_2
}
}
serial_ad_3_event_create_data = {
'label': 'serial_ad_view_3',
"data": {
'user': self.user_1_uuid,
'serial': serial_id,
'ad': ad_3
}
}
self.assertEqual(Event.objects.count(), 0)
response = self.client.post(
reverse('promotions:promotion-create', kwargs={"plan": str(self.serial_promotion_plan.pk)}),
serial_ad_1_event_create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
response = self.client.post(
reverse('promotions:promotion-create', kwargs={"plan": str(self.serial_promotion_plan.pk)}),
serial_ad_2_event_create_data,
HTTP_AUTHORIZATION=auth, format='json')
# response = self.client.post(reverse('promotions:event-create'), serial_ad_2_event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
# event_2 = Event.objects.first()
return
response = self.client.post(reverse('promotions:event-create'), serial_ad_1_event_create_data,
HTTP_AUTHORIZATION=auth, format='json')
event_1 = Event.objects.first()
response = self.client.post(reverse('promotions:event-create'), serial_ad_2_event_create_data,
HTTP_AUTHORIZATION=auth, format='json')
event_2 = Event.objects.first()
response = self.client.post(reverse('promotions:event-create'), serial_ad_3_event_create_data,
HTTP_AUTHORIZATION=auth, format='json')
event_3 = Event.objects.first()
analyze_event_task(event_1.uuid)
analyze_event_task(event_2.uuid)
analyze_event_task(event_3.uuid)
print(response.json())
self.serial_promotion_plan.refresh_from_db()
self.assertEqual(self.serial_promotion_plan.balance, 100000)
self.assertEqual(Event.objects.count(), 3)
def test_plan_balance_from_another_promotion_success(self):
base_plan, _created = Plan.objects.get_or_create(
title='base_plan',
balance=0,
user=self.user_1,
application=self.application,
event_list=['advertising:manual']
)
base_promotion, _created = Promotion.objects.get_or_create(
user_uuid=self.user_1_uuid,
application=self.application,
plan=base_plan,
state=PaymentStateChoices.SUCCESS,
base_amount=10000,
promotion_amount=10000
)
promotion_event_saver, _created = EventSaver.objects.get_or_create(
title=self.first_ad_view_reward_label,
event_label=self.first_ad_view_reward_label,
save_once=True,
)
promotion_plan, _created = Plan.objects.get_or_create(
title='promotion_plan',
balance=0,
balance_holder=base_promotion.pk,
user=self.user_1,
application=self.application,
event_list=[self.first_ad_view_reward_label]
)
self.promotion_recipient, _created = Recipient.objects.get_or_create(
label='promotion_recipient',
plan=promotion_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field=self.user_2_uuid,
base_amount_field=1000,
data={}
)
auth = self._create_authorization_header(self.user_access_token.token)
create_data = {
'label': self.first_ad_view_reward_label,
"data": {
'user': self.user_1_uuid,
}
}
self.assertEqual(Event.objects.count(), 0)
response = self.client.post(reverse('promotions:promotion-create', kwargs={'plan': str(promotion_plan.uuid)}),
create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
promotion_plan.refresh_from_db()
base_promotion.refresh_from_db()
self.assertEqual(base_promotion.base_amount, 10000)
self.assertEqual(base_promotion.promotion_amount, 9000)
self.assertEqual(Event.objects.count(), 1)
response = self.client.post(reverse('promotions:promotion-create', kwargs={'plan': str(promotion_plan.uuid)}),
create_data,
HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
def test_application_promote_user_success(self):
self.first_ad_view_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.first_ad_view_reward_label,
event_label=self.first_ad_view_reward_label,
save_once=True,
)
self.first_ad_view_reward_plan, _created = Plan.objects.get_or_create(
title=self.first_ad_view_reward_label,
balance=10000,
user=self.user_1,
application=self.application,
event_list=[self.first_ad_view_reward_label]
)
self.first_ad_view_reward_recipient, _created = Recipient.objects.get_or_create(
label=self.first_ad_view_reward_label,
plan=self.first_ad_view_reward_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field="->event:user",
base_amount_field="event:base_amount",
)
auth = self._create_authorization_header(self.application_access_token.token)
event_create_data = {
'label': self.first_ad_view_reward_label,
"data": {
'user': self.user_1_uuid,
'base_amount': 800
}
}
url = reverse('promotions-application:plan-promote',
kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid": self.user_1_uuid})
# url = reverse('promotions-application:plan-test', kwargs={"pk": self.first_ad_view_reward_plan.uuid, "user_uuid":self.user_1_uuid})
# response = self.client.get(url, event_create_data, HTTP_AUTHORIZATION=auth,
# format='json')
# print(response.json())
# print(response.status_code)
# response = self.client.post(reverse('promotions:user-promote', kwargs={"pk":self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json')
response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
self.first_ad_view_reward_plan.refresh_from_db()
self.assertEqual(self.first_ad_view_reward_plan.balance, 10000 - 800)
# response = self.client.get(reverse('promotions:promotion-list', kwargs={"plan": self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth,
# format='json')
# print(response.json())
# response = self.client.post(reverse('promotions:promotion-create', kwargs={"plan": self.first_ad_view_reward_plan.uuid}), event_create_data, HTTP_AUTHORIZATION=auth, format='json')
# print(response.json())
def test_application_promote_referral_reward_success(self):
self.referral_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.referral_reward_label,
event_label=self.referral_reward_label,
save_once=True,
)
self.referral_reward_plan, _created = Plan.objects.get_or_create(
title=self.referral_reward_label,
balance=10000,
user=self.user_1,
application=self.application,
event_list=[self.referral_reward_label]
)
self.referral_reward_recipient, _created = Recipient.objects.get_or_create(
label=self.referral_reward_label,
plan=self.referral_reward_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field="->event:referral",
base_amount_field="1000",
)
auth = self._create_authorization_header(self.application_access_token.token)
event_create_data = {
'label': self.referral_reward_label,
"data": {
'user': self.user_1_uuid,
'referral': self.user_2_uuid
}
}
url = reverse('promotions-application:application-user-plan-promote',
kwargs={
"pk": str(self.referral_reward_plan.uuid),
"user_uuid": str(self.user_1_uuid)
}
)
response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
self.referral_reward_plan.refresh_from_db()
self.assertEqual(self.referral_reward_plan.balance, 10000 - 1000)
self.assertEqual(response.json()['promotions'][0]['user'], str(self.user_2_uuid))
def test_application_get_promotion_plan_success(self):
self.first_ad_view_reward_event_saver, _created = EventSaver.objects.get_or_create(
title=self.first_ad_view_reward_label,
event_label=self.first_ad_view_reward_label,
save_once=True,
)
self.first_ad_view_reward_plan, _created = Plan.objects.get_or_create(
title=self.first_ad_view_reward_label,
balance=10000,
user=self.user_1,
application=self.application,
event_list=[self.first_ad_view_reward_label]
)
self.first_ad_view_reward_recipient, _created = Recipient.objects.get_or_create(
label=self.first_ad_view_reward_label,
plan=self.first_ad_view_reward_plan,
wallet_uuid=self.user_1_uuid,
recipient_uuid_field="->event:user",
base_amount_field="event:base_amount",
)
auth = self._create_authorization_header(self.application_access_token.token)
event_create_data = {
'label': self.first_ad_view_reward_label,
"data": {
'user': self.user_1_uuid,
'base_amount': 800
}
}
url_promote = reverse('promotions-application:application-user-plan-promote',
kwargs={"pk": self.first_ad_view_reward_plan.uuid,
"user_uuid": '1f9c72e4-3cc3-420d-928b-b0a3d65a4611'})
response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
def test_application_user__user_not_fount__404(self):
self.first_ad_view_reward_plan, _created = Plan.objects.get_or_create(
title=self.first_ad_view_reward_label,
balance=10000,
user=self.user_1,
application=self.application,
event_list=[self.first_ad_view_reward_label]
)
auth = self._create_authorization_header(self.application_access_token.token)
event_create_data = {}
url_promote = reverse('promotions-application:application-user-plan-promote',
kwargs={"pk": self.first_ad_view_reward_plan.uuid,
"user_uuid": '11111111-1111-1111-1111-111111111111'})
response = self.client.post(url_promote, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
print(response.json())
self.assertEqual(response.status_code, 404)
def test_application_event_viewset_create_success(self):
EventSaver.objects.get_or_create(
title=self.first_ad_view_reward_label,
event_label=self.first_ad_view_reward_label,
save_once=False,
)
auth = self._create_authorization_header(self.application_access_token.token)
event_create_data = {
'label': self.first_ad_view_reward_label,
'data': {'foo': 'bar'},
}
url = reverse('promotions-application:event-list', kwargs={'user_uuid': str(self.user_1_uuid)})
response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
self.assertEqual(response.status_code, 201)
event = Event.objects.get(label=self.first_ad_view_reward_label)
self.assertEqual(event.user, self.user_1_uuid)
def test_application_event_viewset_create_duplicate_returns_unprocessable_entity(self):
EventSaver.objects.get_or_create(
title=self.first_ad_view_reward_label,
event_label=self.first_ad_view_reward_label,
save_once=True,
)
auth = self._create_authorization_header(self.application_access_token.token)
event_create_data = {
'label': self.first_ad_view_reward_label,
'data': {'foo': 'bar'},
}
url = reverse('promotions-application:event-list', kwargs={'user_uuid': str(self.user_1_uuid)})
first_response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
self.assertEqual(first_response.status_code, 201)
second_response = self.client.post(url, event_create_data, HTTP_AUTHORIZATION=auth, format='json')
self.assertEqual(second_response.status_code, 422)
def _create_first_ad_create_plan(self):
event_label = 'ads::first-ad-create'
plan_title = 'first-ad-create'
promotion_amount = 30000
EventSaver.objects.get_or_create(
event_label=event_label,
defaults={
'title': plan_title,
'save_once': True,
},
)
plan, _ = Plan.objects.get_or_create(
title=plan_title,
defaults={
'balance': promotion_amount * 1000,
'user': self.user_1,
'application': self.application,
'event_list': [event_label],
'processor': ProcessorTypeChoices.OTHERS,
},
)
Recipient.objects.update_or_create(
label=plan_title,
plan=plan,
defaults={
'recipient_uuid_field': '->event:user',
'base_amount_field': str(promotion_amount),
'wallet_destination': WalletDestinationChoices.ADVERTISING_TRANSIT,
},
)
return plan, event_label, promotion_amount
def test_first_ad_create_event_status_not_processed(self):
plan, event_label, promotion_amount = self._create_first_ad_create_plan()
auth = self._create_authorization_header(self.user_access_token.token)
response = self.client.get(
reverse('promotions:event-status', kwargs={'event_label': event_label}),
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
'event_label': event_label,
'processed': False,
'promotion_amount': promotion_amount,
})
def test_first_ad_create_event_status_processed(self):
plan, event_label, promotion_amount = self._create_first_ad_create_plan()
auth = self._create_authorization_header(self.user_access_token.token)
event_create_data = {
'label': event_label,
'data': {
'user': str(self.user_1_uuid),
},
}
with patch('apps.promotions.models.deposit_to_user_wallet_submit',
side_effect=mock_submit_deposit_success) as submit_mock:
response = self.client.post(
reverse('promotions:promotion-create', kwargs={'plan': plan.uuid}),
event_create_data,
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 201)
# first_ad_create is billboard/ad credit, not a cash-like user reward: the deposit
# is drawn from the promotions transit pool and credited to the advertising
# transit wallet, not the user's own reward wallet.
from django.conf import settings
submit_mock.assert_called_once()
call_payer_wallet, call_data = submit_mock.call_args.args
self.assertEqual(call_payer_wallet, settings.WALLET_PROMOTIONS_CREDIT)
self.assertEqual(call_data['payee_wallet'], settings.WALLET_ADVERTISING_TRANSIT)
response = self.client.get(
reverse('promotions:event-status', kwargs={'event_label': event_label}),
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
'event_label': event_label,
'processed': True,
'promotion_amount': None,
})
plan.refresh_from_db()
self.assertEqual(plan.balance, promotion_amount * 1000 - promotion_amount)
def test_get_wallet_category_uuid_routing(self):
from django.conf import settings
default_recipient = Recipient()
self.assertEqual(default_recipient.get_wallet_category_uuid(),
settings.WALLET_USER_BILLBOARD_VISIT_INCOME)
user_reward_recipient = Recipient(wallet_destination=WalletDestinationChoices.USER_REWARD)
self.assertEqual(user_reward_recipient.get_wallet_category_uuid(),
settings.WALLET_USER_BILLBOARD_VISIT_INCOME)
transit_recipient = Recipient(wallet_destination=WalletDestinationChoices.ADVERTISING_TRANSIT)
self.assertEqual(transit_recipient.get_wallet_category_uuid(), settings.WALLET_ADVERTISING_TRANSIT)
explicit_wallet_uuid = uuid.uuid4()
override_recipient = Recipient(wallet_destination=WalletDestinationChoices.ADVERTISING_TRANSIT,
wallet_uuid=explicit_wallet_uuid)
self.assertEqual(override_recipient.get_wallet_category_uuid(), explicit_wallet_uuid)
def test_first_ad_create_event_status_not_processed_when_only_event_exists(self):
plan, event_label, promotion_amount = self._create_first_ad_create_plan()
auth = self._create_authorization_header(self.user_access_token.token)
Event.objects.create(
user=self.user_1_uuid,
application=self.application,
label=event_label,
data={'user': str(self.user_1_uuid)},
)
response = self.client.get(
reverse('promotions:event-status', kwargs={'event_label': event_label}),
HTTP_AUTHORIZATION=auth,
format='json',
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
'event_label': event_label,
'processed': False,
'promotion_amount': promotion_amount,
})

22
apps/promotions/urls.py Normal file
View file

@ -0,0 +1,22 @@
from rest_framework import routers
from rest_framework.routers import DefaultRouter, DynamicRoute, Route
from utils.router import ProfileRouter
from . import views
from django.urls import NoReverseMatch, path, re_path, include
app_name = 'promotions'
application_router = DefaultRouter()
event_router = DefaultRouter()
application_router.register(r'promotion', views.ApplicationPromotionViewSet, basename='application_promotions' )
event_router.register(r'events', views.ApplicationEventViewSet, basename='application_events' )
user_router = DefaultRouter()
user_router.register('plan', views.UserPlanViewSet, basename='user')
urlpatterns = [
path('api/v1/application/<user_uuid>/', include(application_router.urls), name='application'),
path('api/v1/application/', include(event_router.urls), name='events'),
path('api/v1/user/', include(user_router.urls), name='user'),
]

View file

@ -1,61 +0,0 @@
from rest_framework import routers
from rest_framework.routers import DefaultRouter, DynamicRoute, Route
from . import views_application
from django.urls import NoReverseMatch, path, re_path
app_name = 'promotions-application'
class ApplicationRouter(DefaultRouter):
routes = [
# List route.
Route(
url=r'^{prefix}/<user_uuid>{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
detail=False,
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes. Generated using
# @action(detail=False) decorator on methods of the viewset.
DynamicRoute(
url=r'^{prefix}/<user_uuid>/{url_path}{trailing_slash}$',
name='{basename}-{url_name}',
detail=False,
initkwargs={}
),
# # Detail route.
Route(
url=r'^{prefix}/<user_uuid>/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
detail=True,
initkwargs={'suffix': 'Instance'}
),
# # Dynamically generated detail routes. Generated using
# # @action(detail=True) decorator on methods of the viewset.
DynamicRoute(
url=r'^{prefix}/<user_uuid>/{lookup}/{url_path}{trailing_slash}$',
name='{basename}-{url_name}',
detail=True,
initkwargs={}
),
]
router = DefaultRouter()
router.register('plan', views_application.ApplicationUserPlanViewSet, basename='application-user-plan')
# router.register('plan', views_application.ApplicationPlanViewSet, basename='application-plan')
router.register('event', views_application.ApplicationEventViewSet, basename='event')
urlpatterns = router.urls

View file

@ -1,31 +0,0 @@
from rest_framework import routers
from rest_framework.routers import DefaultRouter, DynamicRoute, Route
from utils.router import ProfileRouter
from . import views_user
from django.urls import NoReverseMatch, path, re_path, include
from .views_user import (
ApplicationPromoteUserApiView,
ApplicationEventSubmitAPIView,
ApplicationPromotionListApiView,
ApplicationEventStatusApiView,
)
app_name = 'promotions'
router = DefaultRouter()
# router.register(r'application', views.ApplicationPromotionViewSet, basename='application_promotions')
# router.register(r'events', views.ApplicationViewSet, basename='application-events')
router.register(r'plans', views_user.UserPlanViewSet, basename='user-plans')
# urlpatterns =
urlpatterns = [
path('api/v1/plans/<plan>/', ApplicationPromoteUserApiView.as_view(), name='promotion-create'),
path('api/v1/plans/<plan>/promotins', ApplicationPromotionListApiView.as_view(), name='promotion-list'),
path('api/v1/events/<path:event_label>/status/', ApplicationEventStatusApiView.as_view(), name='event-status'),
path('api/v1/events/submit', ApplicationEventSubmitAPIView.as_view(), name='event-create'),
# path('api/v1/events/get', ApplicationEventRetrieveAPIView.as_view(), name='event-retrieve'),
path('api/v2/', include(router.urls)),
]

118
apps/promotions/views.py Normal file
View file

@ -0,0 +1,118 @@
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.viewsets import GenericViewSet
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.gooyal_oauth2.utils import get_application
from .models import Plan, Promotion
from .serializers import PlanSerializer, PromotionSerializer, EventSerializer
from .tasks import analyze_event_task
from ..users.models import User
# class ApplicationPlanViewSet(mixins.ListModelMixin,
# mixins.CreateModelMixin,
# GenericViewSet):
# queryset = Plan.objects.all()
# serializer_class = PlanSerializer
#
# permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
# required_alternate_scopes = {
# "POST": [["promotions.application.user-plans:submit"]],
# "GET": [["promotions.application.user-promotions:list-retrieve"]],
# }
#
# def get_queryset(self):
# user_uuid = self.kwargs.get('user_uuid')
# user = User.objects.get(uuid=user_uuid)
# application = get_application(self.request)
# return Plan.objects.filter(application=application)
#
class ApplicationPromotionViewSet(
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericViewSet
):
serializer_class = PromotionSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [["promotions.application.user-promotions:promote"]],
"GET": [["promotions.application.user-promotions:list-retrieve"]],
}
def get_queryset(self):
user_uuid = self.kwargs.get('user_uuid')
user = User.objects.get(uuid=user_uuid)
application = get_application(self.request)
return Promotion.objects.filter(application=application, user=user)
def perform_create(self, serializer):
user_uuid = self.kwargs.get('user_uuid')
user = User.objects.get(uuid=user_uuid)
application = get_application(self.request)
plan = serializer.validated_data['plan']
promotion_args = dict(user=user, application=application, **serializer.validated_data)
promotion_amount = plan.calculate_promotion(**promotion_args)
if promotion_amount:
promotion = serializer.save(promotion_amount=promotion_amount, user=user, application=application)
promotion.promote(**promotion_args)
class ApplicationEventViewSet(
mixins.CreateModelMixin,
GenericViewSet
):
serializer_class = EventSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [["promotions.application.event:submit"]],
}
def perform_create(self, serializer):
if hasattr(self.request, 'user'):
user = self.request.user
else:
user = None
application = get_application(self.request)
event = serializer.save(user=user, application=application)
analyze_event_task.delay(event.uuid)
class UserPlanViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet):
serializer_class = PlanSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [["promotions.user.self-plans:list-retrieve"]],
}
def get_queryset(self):
user = self.request.user
application = get_application(self.request)
queryset = Plan.objects.all()
return queryset
@extend_schema(responses=PlanSerializer(many=True))
@action(detail=False, methods=['POST'], serializer_class=PlanSerializer)
def available(self, request):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
print(serializer.validated_data)
plans = self.paginate_queryset(self.filter_queryset(self.get_queryset()))
for plan in plans:
plan.calculate_promotion(**serializer.validated_data)
serializer = self.get_serializer(plans, many=True)
return self.get_paginated_response(serializer.data)

View file

@ -1,357 +0,0 @@
from drf_spectacular.utils import extend_schema
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework import exceptions
from rest_framework.exceptions import NotFound
from rest_framework.generics import CreateAPIView, get_object_or_404, RetrieveAPIView, ListAPIView
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.viewsets import GenericViewSet
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, TokenMatchesViewSetActions
from apps.gooyal_oauth2.utils import get_application
from utils.clients.accounts_client import get_user_info
from utils.exceptions import UnprocessableEntity
from .models import Plan, Promotion, EventSaver, get_event_status_for_user, rollback_promotion_payout
from .serializers import (
PlanSerializer,
PromotionSerializer,
EventSerializer,
PromoteSerializer,
UserPlanSerializer,
PromotionStatusSerializer,
PromotionRollbackSerializer,
)
from .tasks import analyze_event_task
from ..users.models import User
from django.utils.translation import gettext as _
class ApplicationUserPlanViewSet(
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
# mixins.CreateModelMixin,
GenericViewSet
):
serializer_class = UserPlanSerializer
permission_classes = [TokenMatchesViewSetActions]
required_action_scopes = {
"promote": [["promotions.application.user-plan:promote"]],
"retrieve": [["promotions.application.user-plan:list-retrieve"]],
"list": [["promotions.application.user-plan:list-retrieve"]],
}
def get_queryset(self):
application = get_application(self.request)
if not self.request.user:
user_uuid = self.kwargs.get('user_uuid')
user = User.objects.filter(uuid=user_uuid).first()
if not user:
user_info = get_user_info(user_uuid)
if user_info:
user, created = User.objects.get_or_create(uuid=user_uuid)
if not user:
raise NotFound(_('user {user_uuid} not found').format(user_uuid=user_uuid))
self.request.user = user
# return Promotion.objects.filter(application=application)
return Plan.objects.all()
# def perform_create(self, serializer):
# user = self.get_user()
# application = get_application(self.request)
#
# plan = serializer.validated_data['plan']
#
# promotion_args = dict(user=user, application=application, **serializer.validated_data)
# promotion_amount = plan.calculate_promotion(**promotion_args)
# if promotion_amount:
# promotion = serializer.save(promotion_amount=promotion_amount, user=user, application=application)
# promotion.promote(**promotion_args)
@action(detail=True, methods=['POST'], serializer_class=PromoteSerializer)
def promote(self, request, *args, pk=None, **kwargs):
plan = self.get_object()
application = get_application(self.request)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
# TODO
# try:
event = event_saver.save_event(user=self.request.user.pk, application=application, **serializer.validated_data)
# except Exception as e:
# raise exceptions.ValidationError(str(e))
result_list = plan.process_event(event)
data["promotions"] = result_list
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
class ApplicationEventViewSet(
mixins.CreateModelMixin,
GenericViewSet
):
serializer_class = EventSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [["promotions.application.event:submit"]],
"GET": [["promotions.application.event:submit"]],
}
def _resolve_user(self):
user = getattr(self.request, 'user', None)
if user and getattr(user, 'uuid', None):
return user
user_uuid = self.kwargs.get('user_uuid')
user = User.objects.filter(uuid=user_uuid).first()
if not user:
user_info = get_user_info(user_uuid)
if user_info:
user, _ = User.objects.get_or_create(uuid=user_uuid)
if not user:
raise NotFound(_('user {user_uuid} not found').format(user_uuid=user_uuid))
self.request.user = user
return user
@action(
detail=False,
methods=['GET'],
url_path=r'(?P<event_label>.+)/status',
serializer_class=PromotionStatusSerializer,
)
def status(self, request, user_uuid=None, event_label=None):
user = self._resolve_user()
serializer = self.get_serializer(get_event_status_for_user(user.uuid, event_label))
return Response(serializer.data)
@action(
detail=False,
methods=['POST'],
url_path=r'(?P<event_label>.+)/rollback',
serializer_class=PromotionRollbackSerializer,
)
def rollback(self, request, user_uuid=None, event_label=None):
"""Pull a promotion's payout back out of the advertising transit wallet.
Idempotent. The promotion stays "used" -- only the money is returned,
and only if it landed in the transit wallet (a payout straight to the
user's wallet is not reversible). If the payout has not been processed
yet the request is recorded and the payout is suppressed when it runs.
"""
user = self._resolve_user()
state, amount = rollback_promotion_payout(user.uuid, event_label)
serializer = self.get_serializer({
'event_label': event_label,
'status': state,
'amount': amount,
})
return Response(serializer.data)
def perform_create(self, serializer: EventSerializer):
user = self._resolve_user()
application = get_application(self.request)
# EventSaver.objects.first(event_label=serializer.validated_data['event_label'], valid_application__contains=application.uuid)
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
try:
event = event_saver.save_event(user=user.uuid, application=application, **serializer.validated_data)
except Exception as e:
raise UnprocessableEntity(str(e))
analyze_event_task.delay(event.uuid)
serializer.instance = event
class ApplicationPlanViewSet(
GenericViewSet
):
serializer_class = PlanSerializer
permission_classes = [TokenMatchesViewSetActions]
required_action_scopes = {
"retrieve": [["promotions.application.plan:list-retrieve"]],
"list": [["promotions.application.plan:list-retrieve"]],
}
def get_queryset(self):
application = get_application(self.request)
# return Promotion.objects.filter(application=application)
return Plan.objects.all()
# NOT
class UserPlanViewSet(mixins.RetrieveModelMixin,
# mixins.ListModelMixin,
# mixins.CreateModelMixin,
GenericViewSet):
serializer_class = UserPlanSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
# required_alternate_scopes = {
# "GET": [["promotions.user.plans:list-retrieve"]],
# "POST": [["promotions.user.plans:list-retrieve"]],
# }
required_alternate_scopes = {
"GET": [[]],
"POST": [[]],
}
def get_queryset(self):
user = self.request.user
application = get_application(self.request)
queryset = Plan.objects.all()
return queryset
# @extend_schema(responses=PlanSerializer(many=True))
# @action(detail=False, methods=['POST'], serializer_class=PlanSerializer)
# def available(self, request):
# serializer = self.get_serializer(data=self.request.data)
# serializer.is_valid(raise_exception=True)
# print(serializer.validated_data)
# plans = self.paginate_queryset(self.filter_queryset(self.get_queryset()))
# for plan in plans:
# plan.calculate_promotion(**serializer.validated_data)
# serializer = self.get_serializer(plans, many=True)
# return self.get_paginated_response(serializer.data)
class ApplicationPromoteUserApiView(CreateAPIView):
model = Promotion
serializer_class = PromoteSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get_plan(self):
plan_uuid = self.kwargs.get('plan')
plan = get_object_or_404(Plan, uuid=plan_uuid)
return plan
def create(self, request, *args, **kwargs):
plan = self.get_plan()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
application = get_application(self.request)
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
user = self.request.user.pk or serializer.validated_data['user']
# try:
event = event_saver.save_event(user=user, application=application, **serializer.validated_data)
# except Exception as e:
# raise exceptions.ValidationError(str(e))
result_list = list(plan.process_event(event))
# data["promotions"] = list(result_list)
serializer = self.get_serializer(instance=plan)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class ApplicationPromotionListApiView(ListAPIView):
model = Promotion
serializer_class = PromotionSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [[]],
}
def get_plan(self):
plan_uuid = self.kwargs.get('plan')
plan = get_object_or_404(Plan, uuid=plan_uuid)
return plan
def get_queryset(self):
user = self.request.user
plan = self.get_plan()
qs = Promotion.objects.filter(user_uuid=user.uuid, plan=plan)
return qs
# def create(self, request, *args, **kwargs):
# plan = self.get_plan()
# serializer = self.get_serializer(data=request.data)
# serializer.is_valid(raise_exception=True)
# data = serializer.validated_data
# application = get_application(self.request)
# event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
# # try:
# event = event_saver.save_event(user=None, application=application, **serializer.validated_data)
# # except Exception as e:
# # raise exceptions.ValidationError(str(e))
#
# result_list = plan.process_event(event)
# data["promotions"] = result_list
#
# headers = self.get_success_headers(serializer.data)
# return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class ApplicationEventSubmitAPIView(CreateAPIView):
model = Promotion
serializer_class = PromoteSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
application = get_application(self.request)
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
user = self.request.user.pk or serializer.validated_data['user']
try:
event = event_saver.save_event(user=user, application=application, **serializer.validated_data)
except Exception as e:
raise UnprocessableEntity(str(e))
analyze_event_task.delay(event.uuid)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
# class ApplicationEventRetrieveAPIView(RetrieveAPIView):
# model = Promotion
# serializer_class = PromoteSerializer
#
# def create(self, request, *args, **kwargs):
# serializer = self.get_serializer(data=request.data)
# serializer.is_valid(raise_exception=True)
# data = serializer.validated_data
# application = get_application(self.request)
# event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
# event = event_saver.save_event(user=None, application=application, **serializer.validated_data)
#
# analyze_event_task.delay(event.uuid)
#
# headers = self.get_success_headers(serializer.data)
# return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
#

View file

@ -1,214 +0,0 @@
from drf_spectacular.utils import extend_schema
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework import exceptions
from rest_framework.generics import CreateAPIView, get_object_or_404, RetrieveAPIView, ListAPIView
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.gooyal_oauth2.utils import get_application
from utils.exceptions import UnprocessableEntity
from .models import Plan, Promotion, EventSaver, get_event_status_for_user
from .serializers import (
PlanSerializer,
PromotionSerializer,
EventSerializer,
PromoteSerializer,
UserPlanSerializer,
PromotionStatusSerializer,
)
from .tasks import analyze_event_task
from ..users.models import User
class UserPlanViewSet(mixins.RetrieveModelMixin,
# mixins.ListModelMixin,
# mixins.CreateModelMixin,
GenericViewSet):
serializer_class = UserPlanSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
# required_alternate_scopes = {
# "GET": [["promotions.user.plans:list-retrieve"]],
# "POST": [["promotions.user.plans:list-retrieve"]],
# }
required_alternate_scopes = {
"GET": [[]],
"POST": [[]],
}
def get_queryset(self):
user = self.request.user
application = get_application(self.request)
queryset = Plan.objects.all()
return queryset
# @extend_schema(responses=PlanSerializer(many=True))
# @action(detail=False, methods=['POST'], serializer_class=PlanSerializer)
# def available(self, request):
# serializer = self.get_serializer(data=self.request.data)
# serializer.is_valid(raise_exception=True)
# print(serializer.validated_data)
# plans = self.paginate_queryset(self.filter_queryset(self.get_queryset()))
# for plan in plans:
# plan.calculate_promotion(**serializer.validated_data)
# serializer = self.get_serializer(plans, many=True)
# return self.get_paginated_response(serializer.data)
@action(detail=True, methods=['POST'], serializer_class=PromoteSerializer)
def promote(self, request, pk=None):
plan = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
application = get_application(self.request)
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
user = self.request.user.pk or serializer.validated_data['user']
# try:
event = event_saver.save_event(user=user, application=application, **serializer.validated_data)
# except Exception as e:
# raise exceptions.ValidationError(str(e))
result_list = plan.process_event(event)
data["promotions"] = result_list
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class ApplicationPromoteUserApiView(CreateAPIView):
model = Promotion
serializer_class = PromoteSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def get_plan(self):
plan_uuid = self.kwargs.get('plan')
plan = get_object_or_404(Plan, uuid=plan_uuid)
return plan
def create(self, request, *args, **kwargs):
plan = self.get_plan()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
application = get_application(self.request)
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
user = self.request.user.pk or serializer.validated_data['user']
# try:
event = event_saver.save_event(user=user, application=application, **serializer.validated_data)
# except Exception as e:
# raise exceptions.ValidationError(str(e))
result_list = list(plan.process_event(event))
# data["promotions"] = list(result_list)
serializer = self.get_serializer(instance=plan)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class ApplicationEventStatusApiView(RetrieveAPIView):
serializer_class = PromotionStatusSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [[]],
}
def retrieve(self, request, *args, **kwargs):
event_label = kwargs['event_label']
user_uuid = request.user.uuid
serializer = self.get_serializer(get_event_status_for_user(user_uuid, event_label))
return Response(serializer.data)
class ApplicationPromotionListApiView(ListAPIView):
model = Promotion
serializer_class = PromotionSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [[]],
}
def get_plan(self):
plan_uuid = self.kwargs.get('plan')
plan = get_object_or_404(Plan, uuid=plan_uuid)
return plan
def get_queryset(self):
user = self.request.user
plan = self.get_plan()
qs = Promotion.objects.filter(user_uuid=user.uuid, plan=plan)
return qs
# def create(self, request, *args, **kwargs):
# plan = self.get_plan()
# serializer = self.get_serializer(data=request.data)
# serializer.is_valid(raise_exception=True)
# data = serializer.validated_data
# application = get_application(self.request)
# event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
# # try:
# event = event_saver.save_event(user=None, application=application, **serializer.validated_data)
# # except Exception as e:
# # raise exceptions.ValidationError(str(e))
#
# result_list = plan.process_event(event)
# data["promotions"] = result_list
#
# headers = self.get_success_headers(serializer.data)
# return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class ApplicationEventSubmitAPIView(CreateAPIView):
model = Promotion
serializer_class = PromoteSerializer
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
required_alternate_scopes = {
"POST": [[]],
}
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
application = get_application(self.request)
event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
user = self.request.user.pk or serializer.validated_data['user']
try:
event = event_saver.save_event(user=user, application=application, **serializer.validated_data)
except Exception as e:
raise UnprocessableEntity(str(e))
analyze_event_task.delay(event.uuid)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
# class ApplicationEventRetrieveAPIView(RetrieveAPIView):
# model = Promotion
# serializer_class = PromoteSerializer
#
# def create(self, request, *args, **kwargs):
# serializer = self.get_serializer(data=request.data)
# serializer.is_valid(raise_exception=True)
# data = serializer.validated_data
# application = get_application(self.request)
# event_saver: EventSaver = EventSaver.objects.filter(event_label=serializer.validated_data['label']).first()
# event = event_saver.save_event(user=None, application=application, **serializer.validated_data)
#
# analyze_event_task.delay(event.uuid)
#
# headers = self.get_success_headers(serializer.data)
# return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
#

View file

@ -1,282 +0,0 @@
# Wallet Refactor — 2026-08-25
Brings promotions' wallet settings and deposit routing in line with the naming/routing
convention already rolled out to `advertising`, `settlement`, and `ipg`. No behavior in
those sibling repos changed as part of this — this document covers promotions only, with
the sibling commits cited as precedent for why the shape of the fix looks the way it does.
---
## 1. The bug
`Promotion.promote()` (`apps/promotions/models.py`) submits every payout as a wallet
deposit. A deposit call takes two wallet references:
- `payer_wallet` — the call-target / URL param — the **company-side** pool the money is
drawn from.
- `payee_wallet` — a field in the request body — the **user-side** wallet type the
recipient is credited into.
Before this change, both were the same setting:
```python
"payee_wallet": settings.WALLET_REWARD,
...
submit_response = deposit_to_user_wallet_submit(settings.WALLET_REWARD, data)
...
verify_response = deposit_to_user_wallet_verify(settings.WALLET_REWARD, submit_response.uuid)
```
So every promotion payout was routed **out of and into the same wallet type** — there was
no real company-owned pool distinct from the user-side wallet category. This is the same
defect fixed in `settlement` (commit `cc2ffb9`, 2026-08-16):
> "Withdraw requests were passing the user's own source wallet (WALLET_RIAL/WALLET_REWARD)
> as the withdrawal's destination wallet param, so every settlement payout and commission
> was routed back into the same wallet type it came from."
and still open, but flagged, in `advertising`'s `wallet_service_integration.md` §6.2 for
`AdPayment.refund_balance()`.
A second, smaller bug rode along: `Recipient.get_wallet_category_uuid()` fell back to
`settings.WALLET_PROMOTION_CATEGORY_UUID` — a setting that was never defined anywhere in
`main/settings.py`. It happened to never be called from the live deposit path (which
hardcoded `WALLET_REWARD` instead), so this was latent, not yet crashing anything — but it
would have raised `AttributeError` the moment anyone wired it in, which is exactly what
this change does.
---
## 2. The fix
### 2.1 Settings renamed to match the shared cross-repo convention
The two user-side wallet-type UUIDs are the same wallet-service UUIDs referenced by
`advertising`, `settlement`, and `ipg` — they're renamed to match those repos' naming
(`advertising`, then propagated to `settlement` in `cc2ffb9` and `ipg` in `7dcb730`):
| Before | After |
|---|---|
| `WALLET_RIAL` | `WALLET_RIAL_DEPOSIT` |
| `WALLET_REWARD` | `WALLET_USER_BILLBOARD_VISIT_INCOME` |
| *(did not exist)* | `WALLET_PROMOTIONS_CREDIT` — new |
`WALLET_RIAL_DEPOSIT` isn't read by any code path in promotions today (it wasn't before
either, under its old name) — it's renamed for consistency and in case a future feature
here needs to read a user's rial balance via `get_user_wallets()`.
### 2.2 A dedicated company-side wallet for payouts
`WALLET_PROMOTIONS_CREDIT` is new: the company pool promotion payouts are drawn from,
used only as `payer_wallet` (the deposit call-target). It is never the same UUID as any
user-side wallet type. This is the promotions equivalent of `WALLET_SETTLEMENT_TRANSIT`
(settlement) / `WALLET_ADVERTISING_TRANSIT` (advertising).
**This is a required new environment variable.** It ships in `.env.example` as a
placeholder UUID (`00000000-...`) with a `TODO(you)` comment — same pattern as
settlement's `.env.example`. **It needs a real wallet-type UUID provisioned by the
wallet-service team before this service can submit a deposit in any environment**, and
your local/staging/prod `.env` files need the rename applied (`WALLET_RIAL` →
`WALLET_RIAL_DEPOSIT`, `WALLET_REWARD` → `WALLET_USER_BILLBOARD_VISIT_INCOME`) plus this
new key added, or `main/settings.py` will fail at startup with
`decouple.UndefinedValueError`.
### 2.3 Per-recipient destination routing wired in
`Recipient.get_wallet_category_uuid()` existed already (`self.wallet_uuid or <fallback>`)
but nothing called it — every payout hardcoded `WALLET_REWARD` as `payee_wallet`
regardless of what a `Recipient` might specify. It's now the actual source of
`payee_wallet` in `Promotion.promote()`:
```python
payee_wallet = self.recipient.get_wallet_category_uuid()
```
So a `Recipient` with its own `wallet_uuid` set now routes its payout to that wallet type
instead of the default; a `Recipient` with `wallet_uuid=None` falls back to
`WALLET_USER_BILLBOARD_VISIT_INCOME`. This mirrors `EscrowWalletPayment.destination_wallet`
in `advertising` — a per-row field read at call time instead of one hardcoded constant —
without inventing a new abstraction: `Recipient.wallet_uuid` and
`get_wallet_category_uuid()` already existed in this codebase, they just weren't
connected to anything.
---
## 3. Before / after, side by side
### `main/settings.py`
```diff
WALLET_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL', default=None, cast=str)
-WALLET_RIAL = config('WALLET_RIAL', cast=str)
-WALLET_REWARD = config('WALLET_REWARD', cast=str)
+WALLET_RIAL_DEPOSIT = config('WALLET_RIAL_DEPOSIT', cast=str)
+WALLET_USER_BILLBOARD_VISIT_INCOME = config('WALLET_USER_BILLBOARD_VISIT_INCOME', cast=str)
+# Company-side pool promotion payouts are drawn from; must be distinct from the user-side
+# wallet above. TODO(you): replace with the real wallet-type UUID from the wallet service.
+WALLET_PROMOTIONS_CREDIT = config('WALLET_PROMOTIONS_CREDIT', cast=str)
```
### `apps/promotions/models.py` — `Recipient.get_wallet_category_uuid()`
```diff
def get_wallet_category_uuid(self):
- return self.wallet_uuid or settings.WALLET_PROMOTION_CATEGORY_UUID
+ return self.wallet_uuid or settings.WALLET_USER_BILLBOARD_VISIT_INCOME
```
### `apps/promotions/models.py` — `Promotion.promote()`
```diff
payment_uuid = str(self.uuid)
+ payee_wallet = self.recipient.get_wallet_category_uuid()
+
data = {
"uuid": payment_uuid,
"payee": str(self.user_uuid),
"payee_type": 1,
- "payee_wallet": settings.WALLET_REWARD,
+ "payee_wallet": payee_wallet,
"amount": self.promotion_amount,
"details": {
'description': str(_(self.recipient.label)),
'reference_id': str(self.pk),
'application_details_url': ''
},
}
try:
- submit_response = deposit_to_user_wallet_submit(settings.WALLET_REWARD, data)
+ submit_response = deposit_to_user_wallet_submit(settings.WALLET_PROMOTIONS_CREDIT, data)
...
try:
- verify_response = deposit_to_user_wallet_verify(settings.WALLET_REWARD, submit_response.uuid)
+ verify_response = deposit_to_user_wallet_verify(settings.WALLET_PROMOTIONS_CREDIT, submit_response.uuid)
```
### What the deposit call looks like now, end to end
| | Before | After |
|---|---|---|
| `payer_wallet` (call-target, company money source) | `WALLET_REWARD` | `WALLET_PROMOTIONS_CREDIT` |
| `payee_wallet` (body, user-side credit type) | `WALLET_REWARD` (same UUID as source) | `Recipient.wallet_uuid`, falling back to `WALLET_USER_BILLBOARD_VISIT_INCOME` |
| Per-recipient routing | Not possible — one hardcoded constant | Possible — set `Recipient.wallet_uuid` |
---
## 4. Files touched
| File | Change |
|---|---|
| `main/settings.py` | Renamed `WALLET_RIAL`→`WALLET_RIAL_DEPOSIT`, `WALLET_REWARD`→`WALLET_USER_BILLBOARD_VISIT_INCOME`; added `WALLET_PROMOTIONS_CREDIT`. |
| `apps/promotions/models.py` | `Recipient.get_wallet_category_uuid()` fallback fixed to point at a setting that actually exists; `Promotion.promote()` now uses `WALLET_PROMOTIONS_CREDIT` as `payer_wallet` and `recipient.get_wallet_category_uuid()` as `payee_wallet`. |
| `.env.example` | New — didn't exist before. Documents every `config()` var read by `main/settings.py`, including the new wallet keys as placeholders. |
| `README.md` | Config reference table updated to the renamed/new settings. |
Not touched: `apps/promotions/tests.py` — its wallet mocks patch the client functions
directly (`patch('apps.promotions.models.deposit_to_user_wallet_submit', ...)`) rather
than asserting on which UUID was passed, so they don't need updating for this change, but
they also don't exercise the routing fix — there's no test asserting `payer_wallet` /
`payee_wallet` on the call. `apps/promotions/handlers.py` (the dead processor/handler
scaffolding noted in the README's watch list) — unrelated, left as-is.
---
## 5. What you need to do before this runs anywhere
1. Get a real wallet-type UUID for `WALLET_PROMOTIONS_CREDIT` from the wallet-service
team — it must be a genuine, dedicated pool, not a reused existing UUID (that's the
exact bug this change fixes).
2. In every environment's `.env` (local, staging, prod — none are checked into this repo):
- Rename `WALLET_RIAL` → `WALLET_RIAL_DEPOSIT` (same value, key renamed).
- Rename `WALLET_REWARD` → `WALLET_USER_BILLBOARD_VISIT_INCOME` (same value, key renamed).
- Add `WALLET_PROMOTIONS_CREDIT` with the real UUID from step 1.
3. Until step 2 is done in a given environment, `main/settings.py` will fail to import
with `decouple.UndefinedValueError: WALLET_RIAL_DEPOSIT not found` — the service won't
start at all, not just fail at payout time. Treat this as a deploy-blocking config
change, not a code-only one.
---
## 6. Follow-up: explicit per-recipient wallet destination (2026-08-25)
Not every payout is a cash-like user reward. Some fund billboard/ad credit instead — money
that should land in the **advertising** service's own transit wallet, not the user's
personal reward wallet.
An earlier version of this follow-up tried to infer the destination from
`PromotionTypeChoices` (`first_ad_view` / `capture` / `first_ad_create`), a promotion
*category* field that existed but had never been given real values. That was reverted: it
buried a wallet-routing decision inside a general-purpose categorization field, coupling
two things that should vary independently — a promotion's category and where its money
goes are not the same fact, and the next new promotion type would need someone to remember
to also classify it for wallet purposes.
Instead, `Recipient` gets a field that says the routing decision directly:
```python
class WalletDestinationChoices(models.TextChoices):
USER_REWARD = 'user_reward', _('user reward wallet')
ADVERTISING_TRANSIT = 'advertising_transit', _('advertising transit wallet')
```
| `wallet_destination` | Nature | Resolves to |
|---|---|---|
| `user_reward` (default) | Cash-like user reward | `WALLET_USER_BILLBOARD_VISIT_INCOME` |
| `advertising_transit` | Billboard/ad credit | `WALLET_ADVERTISING_TRANSIT` (advertising's own company pool) |
`PromotionTypeChoices` is left as it was before any of this — an empty enum, unused. It's
not part of this decision.
### Changes
- `main/settings.py` — new `WALLET_ADVERTISING_TRANSIT` setting. Same wallet-service UUID
as advertising's own setting of the same name — copy that repo's real value in, don't
provision a second UUID for the same wallet.
- `apps/promotions/models.py`:
- New `WalletDestinationChoices` enum, next to the existing `RecipientTypeChoices`.
- New `Recipient.wallet_destination` field (`CharField`, `db_index=True`, default
`USER_REWARD`) — a real DB column, unlike the earlier `promotion_type` attempt which
only changed field-level `choices=` metadata.
- `Recipient.get_wallet_category_uuid()`:
```python
def get_wallet_category_uuid(self):
if self.wallet_uuid:
return self.wallet_uuid
if self.wallet_destination == WalletDestinationChoices.ADVERTISING_TRANSIT:
return settings.WALLET_ADVERTISING_TRANSIT
return settings.WALLET_USER_BILLBOARD_VISIT_INCOME
```
Priority order: an explicit `Recipient.wallet_uuid` always wins (the per-recipient raw
override from the original refactor above); otherwise `wallet_destination` picks the
wallet type; `user_reward` is the default so existing rows behave exactly as before
this change until someone opts them into `advertising_transit`.
- `apps/promotions/migrations/0010_recipient_wallet_destination.py` — new migration adding
the column, `AddField` with `default='user_reward'` so existing rows backfill safely.
- `apps/promotions/tests.py` — the `first-ad-create` fixture now sets
`wallet_destination=WalletDestinationChoices.ADVERTISING_TRANSIT`;
`test_first_ad_create_event_status_processed` asserts the deposit call actually receives
`WALLET_PROMOTIONS_CREDIT` as `payer_wallet` and `WALLET_ADVERTISING_TRANSIT` as
`payee_wallet`; `test_get_wallet_category_uuid_routing` unit-tests all four routing cases
directly against `Recipient.get_wallet_category_uuid()`.
- `README.md` — config reference table and the playbook's step 3 example updated to show
setting `wallet_destination` on a new `Recipient`.
### What's still manual
- Existing `Plan`/`Recipient` rows in a live database default to `wallet_destination='user_reward'`
on migrate — behavior for them doesn't change. Whoever owns the real `capture` /
`first-ad-create` plans needs to explicitly set `wallet_destination='advertising_transit'`
on their `Recipient` rows (via admin or a follow-up data migration) for those specific
payouts to actually route to the advertising transit wallet.
- `WALLET_ADVERTISING_TRANSIT` is a second **required** env var on top of
`WALLET_PROMOTIONS_CREDIT` — same deploy-blocking caveat as §5: missing it fails
`main/settings.py` import, not just a payout at runtime.
- This migration hasn't been run against a real database in this environment (no local
`.env`/DB configured here) — run `manage.py migrate` and confirm `0010` applies cleanly
before deploying.

View file

@ -5,22 +5,24 @@ import httpx
from ... import errors from ... import errors
from ...client import AuthenticatedClient, Client from ...client import AuthenticatedClient, Client
from ...models.account import Account from ...models.application_balance import ApplicationBalance
from ...types import Response from ...types import Response
def _get_kwargs() -> dict[str, Any]: def _get_kwargs() -> dict[str, Any]:
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": "/users/api/account/", "url": "/wallet/api/application/balance",
} }
return _kwargs return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Account]: def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ApplicationBalance]:
if response.status_code == 200: if response.status_code == 200:
response_200 = Account.from_dict(response.json()) response_200 = ApplicationBalance.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
@ -29,7 +31,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Account]: def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ApplicationBalance]:
return Response( return Response(
status_code=HTTPStatus(response.status_code), status_code=HTTPStatus(response.status_code),
content=response.content, content=response.content,
@ -41,14 +45,15 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt
def sync_detailed( def sync_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[Account]: ) -> Response[ApplicationBalance]:
""" """API for getting specified user or application balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[Account] Response[ApplicationBalance]
""" """
kwargs = _get_kwargs() kwargs = _get_kwargs()
@ -63,14 +68,15 @@ def sync_detailed(
def sync( def sync(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[Account]: ) -> Optional[ApplicationBalance]:
""" """API for getting specified user or application balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Account ApplicationBalance
""" """
return sync_detailed( return sync_detailed(
@ -81,14 +87,15 @@ def sync(
async def asyncio_detailed( async def asyncio_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[Account]: ) -> Response[ApplicationBalance]:
""" """API for getting specified user or application balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[Account] Response[ApplicationBalance]
""" """
kwargs = _get_kwargs() kwargs = _get_kwargs()
@ -101,14 +108,15 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[Account]: ) -> Optional[ApplicationBalance]:
""" """API for getting specified user or application balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Account ApplicationBalance
""" """
return ( return (

View file

@ -10,7 +10,6 @@ from ...types import Response
def _get_kwargs( def _get_kwargs(
payer_wallet: str,
*, *,
body: ApplicationDeposit, body: ApplicationDeposit,
) -> dict[str, Any]: ) -> dict[str, Any]:
@ -18,11 +17,12 @@ def _get_kwargs(
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "post", "method": "post",
"url": f"/wallet/api/application/{payer_wallet}/deposit/", "url": "/wallet/api/application/deposit/",
} }
_kwargs["json"] = body.to_dict() _body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers _kwargs["headers"] = headers
@ -36,7 +36,6 @@ def _parse_response(
response_201 = ApplicationDeposit.from_dict(response.json()) response_201 = ApplicationDeposit.from_dict(response.json())
return response_201 return response_201
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content) raise errors.UnexpectedStatus(response.status_code, response.content)
else: else:
@ -55,14 +54,12 @@ def _build_response(
def sync_detailed( def sync_detailed(
payer_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationDeposit, body: ApplicationDeposit,
) -> Response[ApplicationDeposit]: ) -> Response[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
body (ApplicationDeposit): body (ApplicationDeposit):
Raises: Raises:
@ -74,7 +71,6 @@ def sync_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payer_wallet=payer_wallet,
body=body, body=body,
) )
@ -86,14 +82,12 @@ def sync_detailed(
def sync( def sync(
payer_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationDeposit, body: ApplicationDeposit,
) -> Optional[ApplicationDeposit]: ) -> Optional[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
body (ApplicationDeposit): body (ApplicationDeposit):
Raises: Raises:
@ -105,21 +99,18 @@ def sync(
""" """
return sync_detailed( return sync_detailed(
payer_wallet=payer_wallet,
client=client, client=client,
body=body, body=body,
).parsed ).parsed
async def asyncio_detailed( async def asyncio_detailed(
payer_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationDeposit, body: ApplicationDeposit,
) -> Response[ApplicationDeposit]: ) -> Response[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
body (ApplicationDeposit): body (ApplicationDeposit):
Raises: Raises:
@ -131,7 +122,6 @@ async def asyncio_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payer_wallet=payer_wallet,
body=body, body=body,
) )
@ -141,14 +131,12 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
payer_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationDeposit, body: ApplicationDeposit,
) -> Optional[ApplicationDeposit]: ) -> Optional[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
body (ApplicationDeposit): body (ApplicationDeposit):
Raises: Raises:
@ -161,7 +149,6 @@ async def asyncio(
return ( return (
await asyncio_detailed( await asyncio_detailed(
payer_wallet=payer_wallet,
client=client, client=client,
body=body, body=body,
) )

View file

@ -10,12 +10,11 @@ from ...types import Response
def _get_kwargs( def _get_kwargs(
payer_wallet: str,
uuid: str, uuid: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": f"/wallet/api/application/{payer_wallet}/deposit/{uuid}/verify", "url": f"/wallet/api/application/deposit/{uuid}/verify",
} }
return _kwargs return _kwargs
@ -28,7 +27,6 @@ def _parse_response(
response_200 = ApplicationDeposit.from_dict(response.json()) response_200 = ApplicationDeposit.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content) raise errors.UnexpectedStatus(response.status_code, response.content)
else: else:
@ -47,14 +45,12 @@ def _build_response(
def sync_detailed( def sync_detailed(
payer_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[ApplicationDeposit]: ) -> Response[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -66,7 +62,6 @@ def sync_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payer_wallet=payer_wallet,
uuid=uuid, uuid=uuid,
) )
@ -78,14 +73,12 @@ def sync_detailed(
def sync( def sync(
payer_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[ApplicationDeposit]: ) -> Optional[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -97,21 +90,18 @@ def sync(
""" """
return sync_detailed( return sync_detailed(
payer_wallet=payer_wallet,
uuid=uuid, uuid=uuid,
client=client, client=client,
).parsed ).parsed
async def asyncio_detailed( async def asyncio_detailed(
payer_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[ApplicationDeposit]: ) -> Response[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -123,7 +113,6 @@ async def asyncio_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payer_wallet=payer_wallet,
uuid=uuid, uuid=uuid,
) )
@ -133,14 +122,12 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
payer_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[ApplicationDeposit]: ) -> Optional[ApplicationDeposit]:
""" """
Args: Args:
payer_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -153,7 +140,6 @@ async def asyncio(
return ( return (
await asyncio_detailed( await asyncio_detailed(
payer_wallet=payer_wallet,
uuid=uuid, uuid=uuid,
client=client, client=client,
) )

View file

@ -5,7 +5,7 @@ import httpx
from ... import errors from ... import errors
from ...client import AuthenticatedClient, Client from ...client import AuthenticatedClient, Client
from ...models.paginated_public_user_list import PaginatedPublicUserList from ...models.paginated_application_transaction_list import PaginatedApplicationTransactionList
from ...types import UNSET, Response, Unset from ...types import UNSET, Response, Unset
@ -21,7 +21,7 @@ def _get_kwargs(
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": "/users/api/users/", "url": "/wallet/api/application/",
"params": params, "params": params,
} }
@ -30,9 +30,9 @@ def _get_kwargs(
def _parse_response( def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[PaginatedPublicUserList]: ) -> Optional[PaginatedApplicationTransactionList]:
if response.status_code == 200: if response.status_code == 200:
response_200 = PaginatedPublicUserList.from_dict(response.json()) response_200 = PaginatedApplicationTransactionList.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
@ -43,7 +43,7 @@ def _parse_response(
def _build_response( def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[PaginatedPublicUserList]: ) -> Response[PaginatedApplicationTransactionList]:
return Response( return Response(
status_code=HTTPStatus(response.status_code), status_code=HTTPStatus(response.status_code),
content=response.content, content=response.content,
@ -56,7 +56,7 @@ def sync_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Response[PaginatedPublicUserList]: ) -> Response[PaginatedApplicationTransactionList]:
""" """
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -66,7 +66,7 @@ def sync_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PaginatedPublicUserList] Response[PaginatedApplicationTransactionList]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@ -84,7 +84,7 @@ def sync(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Optional[PaginatedPublicUserList]: ) -> Optional[PaginatedApplicationTransactionList]:
""" """
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -94,7 +94,7 @@ def sync(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PaginatedPublicUserList PaginatedApplicationTransactionList
""" """
return sync_detailed( return sync_detailed(
@ -107,7 +107,7 @@ async def asyncio_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Response[PaginatedPublicUserList]: ) -> Response[PaginatedApplicationTransactionList]:
""" """
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -117,7 +117,7 @@ async def asyncio_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PaginatedPublicUserList] Response[PaginatedApplicationTransactionList]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@ -133,7 +133,7 @@ async def asyncio(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Optional[PaginatedPublicUserList]: ) -> Optional[PaginatedApplicationTransactionList]:
""" """
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -143,7 +143,7 @@ async def asyncio(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PaginatedPublicUserList PaginatedApplicationTransactionList
""" """
return ( return (

View file

@ -1,29 +1,30 @@
from http import HTTPStatus from http import HTTPStatus
from typing import Any, Optional, Union from typing import Any, Optional, Union
from uuid import UUID
import httpx import httpx
from ... import errors from ... import errors
from ...client import AuthenticatedClient, Client from ...client import AuthenticatedClient, Client
from ...models.public_user import PublicUser from ...models.application_transaction import ApplicationTransaction
from ...types import Response from ...types import Response
def _get_kwargs( def _get_kwargs(
uuid: UUID, uuid: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": f"/users/api/users/{uuid}/", "url": f"/wallet/api/application/{uuid}/",
} }
return _kwargs return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[PublicUser]: def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ApplicationTransaction]:
if response.status_code == 200: if response.status_code == 200:
response_200 = PublicUser.from_dict(response.json()) response_200 = ApplicationTransaction.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
@ -32,7 +33,9 @@ def _parse_response(*, client: Union[AuthenticatedClient, Client], response: htt
return None return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[PublicUser]: def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ApplicationTransaction]:
return Response( return Response(
status_code=HTTPStatus(response.status_code), status_code=HTTPStatus(response.status_code),
content=response.content, content=response.content,
@ -42,20 +45,20 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt
def sync_detailed( def sync_detailed(
uuid: UUID, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[PublicUser]: ) -> Response[ApplicationTransaction]:
""" """
Args: Args:
uuid (UUID): uuid (str):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PublicUser] Response[ApplicationTransaction]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@ -70,20 +73,20 @@ def sync_detailed(
def sync( def sync(
uuid: UUID, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[PublicUser]: ) -> Optional[ApplicationTransaction]:
""" """
Args: Args:
uuid (UUID): uuid (str):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PublicUser ApplicationTransaction
""" """
return sync_detailed( return sync_detailed(
@ -93,20 +96,20 @@ def sync(
async def asyncio_detailed( async def asyncio_detailed(
uuid: UUID, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[PublicUser]: ) -> Response[ApplicationTransaction]:
""" """
Args: Args:
uuid (UUID): uuid (str):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PublicUser] Response[ApplicationTransaction]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@ -119,20 +122,20 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
uuid: UUID, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[PublicUser]: ) -> Optional[ApplicationTransaction]:
""" """
Args: Args:
uuid (UUID): uuid (str):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PublicUser ApplicationTransaction
""" """
return ( return (

View file

@ -10,7 +10,6 @@ from ...types import Response
def _get_kwargs( def _get_kwargs(
payee_wallet: str,
*, *,
body: ApplicationWithdraw, body: ApplicationWithdraw,
) -> dict[str, Any]: ) -> dict[str, Any]:
@ -18,11 +17,12 @@ def _get_kwargs(
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "post", "method": "post",
"url": f"/wallet/api/application/{payee_wallet}/withdraw/", "url": "/wallet/api/application/withdraw/",
} }
_kwargs["json"] = body.to_dict() _body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers _kwargs["headers"] = headers
@ -36,7 +36,6 @@ def _parse_response(
response_201 = ApplicationWithdraw.from_dict(response.json()) response_201 = ApplicationWithdraw.from_dict(response.json())
return response_201 return response_201
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content) raise errors.UnexpectedStatus(response.status_code, response.content)
else: else:
@ -55,14 +54,12 @@ def _build_response(
def sync_detailed( def sync_detailed(
payee_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationWithdraw, body: ApplicationWithdraw,
) -> Response[ApplicationWithdraw]: ) -> Response[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
body (ApplicationWithdraw): body (ApplicationWithdraw):
Raises: Raises:
@ -74,7 +71,6 @@ def sync_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payee_wallet=payee_wallet,
body=body, body=body,
) )
@ -86,14 +82,12 @@ def sync_detailed(
def sync( def sync(
payee_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationWithdraw, body: ApplicationWithdraw,
) -> Optional[ApplicationWithdraw]: ) -> Optional[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
body (ApplicationWithdraw): body (ApplicationWithdraw):
Raises: Raises:
@ -105,21 +99,18 @@ def sync(
""" """
return sync_detailed( return sync_detailed(
payee_wallet=payee_wallet,
client=client, client=client,
body=body, body=body,
).parsed ).parsed
async def asyncio_detailed( async def asyncio_detailed(
payee_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationWithdraw, body: ApplicationWithdraw,
) -> Response[ApplicationWithdraw]: ) -> Response[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
body (ApplicationWithdraw): body (ApplicationWithdraw):
Raises: Raises:
@ -131,7 +122,6 @@ async def asyncio_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payee_wallet=payee_wallet,
body=body, body=body,
) )
@ -141,14 +131,12 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
payee_wallet: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
body: ApplicationWithdraw, body: ApplicationWithdraw,
) -> Optional[ApplicationWithdraw]: ) -> Optional[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
body (ApplicationWithdraw): body (ApplicationWithdraw):
Raises: Raises:
@ -161,7 +149,6 @@ async def asyncio(
return ( return (
await asyncio_detailed( await asyncio_detailed(
payee_wallet=payee_wallet,
client=client, client=client,
body=body, body=body,
) )

View file

@ -10,12 +10,11 @@ from ...types import Response
def _get_kwargs( def _get_kwargs(
payee_wallet: str,
uuid: str, uuid: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": f"/wallet/api/application/{payee_wallet}/withdraw/{uuid}/verify", "url": f"/wallet/api/application/withdraw/{uuid}/verify",
} }
return _kwargs return _kwargs
@ -28,7 +27,6 @@ def _parse_response(
response_200 = ApplicationWithdraw.from_dict(response.json()) response_200 = ApplicationWithdraw.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content) raise errors.UnexpectedStatus(response.status_code, response.content)
else: else:
@ -47,14 +45,12 @@ def _build_response(
def sync_detailed( def sync_detailed(
payee_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[ApplicationWithdraw]: ) -> Response[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -66,7 +62,6 @@ def sync_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payee_wallet=payee_wallet,
uuid=uuid, uuid=uuid,
) )
@ -78,14 +73,12 @@ def sync_detailed(
def sync( def sync(
payee_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[ApplicationWithdraw]: ) -> Optional[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -97,21 +90,18 @@ def sync(
""" """
return sync_detailed( return sync_detailed(
payee_wallet=payee_wallet,
uuid=uuid, uuid=uuid,
client=client, client=client,
).parsed ).parsed
async def asyncio_detailed( async def asyncio_detailed(
payee_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[ApplicationWithdraw]: ) -> Response[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -123,7 +113,6 @@ async def asyncio_detailed(
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
payee_wallet=payee_wallet,
uuid=uuid, uuid=uuid,
) )
@ -133,14 +122,12 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
payee_wallet: str,
uuid: str, uuid: str,
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[ApplicationWithdraw]: ) -> Optional[ApplicationWithdraw]:
""" """
Args: Args:
payee_wallet (str):
uuid (str): uuid (str):
Raises: Raises:
@ -153,7 +140,6 @@ async def asyncio(
return ( return (
await asyncio_detailed( await asyncio_detailed(
payee_wallet=payee_wallet,
uuid=uuid, uuid=uuid,
client=client, client=client,
) )

View file

@ -5,7 +5,7 @@ import httpx
from ... import errors from ... import errors
from ...client import AuthenticatedClient, Client from ...client import AuthenticatedClient, Client
from ...models.paginated_application_list import PaginatedApplicationList from ...models.paginated_user_transaction_list import PaginatedUserTransactionList
from ...types import UNSET, Response, Unset from ...types import UNSET, Response, Unset
@ -21,7 +21,7 @@ def _get_kwargs(
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": "/oauth2/apis/applications/", "url": "/wallet/api/transactions/",
"params": params, "params": params,
} }
@ -30,9 +30,9 @@ def _get_kwargs(
def _parse_response( def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[PaginatedApplicationList]: ) -> Optional[PaginatedUserTransactionList]:
if response.status_code == 200: if response.status_code == 200:
response_200 = PaginatedApplicationList.from_dict(response.json()) response_200 = PaginatedUserTransactionList.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
@ -43,7 +43,7 @@ def _parse_response(
def _build_response( def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[PaginatedApplicationList]: ) -> Response[PaginatedUserTransactionList]:
return Response( return Response(
status_code=HTTPStatus(response.status_code), status_code=HTTPStatus(response.status_code),
content=response.content, content=response.content,
@ -56,8 +56,13 @@ def sync_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Response[PaginatedApplicationList]: ) -> Response[PaginatedUserTransactionList]:
""" """GET: API for getting list of transactions for authenticated user
POST: API for creating transaction for transferring funds from authenticated user to application
owner. (deprecated)
args:
- user: authenticated user
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -66,7 +71,7 @@ def sync_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PaginatedApplicationList] Response[PaginatedUserTransactionList]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@ -84,8 +89,13 @@ def sync(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Optional[PaginatedApplicationList]: ) -> Optional[PaginatedUserTransactionList]:
""" """GET: API for getting list of transactions for authenticated user
POST: API for creating transaction for transferring funds from authenticated user to application
owner. (deprecated)
args:
- user: authenticated user
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -94,7 +104,7 @@ def sync(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PaginatedApplicationList PaginatedUserTransactionList
""" """
return sync_detailed( return sync_detailed(
@ -107,8 +117,13 @@ async def asyncio_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Response[PaginatedApplicationList]: ) -> Response[PaginatedUserTransactionList]:
""" """GET: API for getting list of transactions for authenticated user
POST: API for creating transaction for transferring funds from authenticated user to application
owner. (deprecated)
args:
- user: authenticated user
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -117,7 +132,7 @@ async def asyncio_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PaginatedApplicationList] Response[PaginatedUserTransactionList]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@ -133,8 +148,13 @@ async def asyncio(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
page: Union[Unset, int] = UNSET, page: Union[Unset, int] = UNSET,
) -> Optional[PaginatedApplicationList]: ) -> Optional[PaginatedUserTransactionList]:
""" """GET: API for getting list of transactions for authenticated user
POST: API for creating transaction for transferring funds from authenticated user to application
owner. (deprecated)
args:
- user: authenticated user
Args: Args:
page (Union[Unset, int]): page (Union[Unset, int]):
@ -143,7 +163,7 @@ async def asyncio(
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PaginatedApplicationList PaginatedUserTransactionList
""" """
return ( return (

View file

@ -5,32 +5,31 @@ import httpx
from ... import errors from ... import errors
from ...client import AuthenticatedClient, Client from ...client import AuthenticatedClient, Client
from ...models.push_user import PushUser from ...models.my_wallet import MyWallet
from ...types import Response from ...types import Response
def _get_kwargs() -> dict[str, Any]: def _get_kwargs() -> dict[str, Any]:
_kwargs: dict[str, Any] = { _kwargs: dict[str, Any] = {
"method": "get", "method": "get",
"url": "/push/user/push_user/", "url": "/wallet/api/wallet/balance",
} }
return _kwargs return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[PushUser]: def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[MyWallet]:
if response.status_code == 200: if response.status_code == 200:
response_200 = PushUser.from_dict(response.json()) response_200 = MyWallet.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content) raise errors.UnexpectedStatus(response.status_code, response.content)
else: else:
return None return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[PushUser]: def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[MyWallet]:
return Response( return Response(
status_code=HTTPStatus(response.status_code), status_code=HTTPStatus(response.status_code),
content=response.content, content=response.content,
@ -42,14 +41,15 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt
def sync_detailed( def sync_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[PushUser]: ) -> Response[MyWallet]:
""" """API for getting authenticated user balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PushUser] Response[MyWallet]
""" """
kwargs = _get_kwargs() kwargs = _get_kwargs()
@ -64,14 +64,15 @@ def sync_detailed(
def sync( def sync(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[PushUser]: ) -> Optional[MyWallet]:
""" """API for getting authenticated user balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PushUser MyWallet
""" """
return sync_detailed( return sync_detailed(
@ -82,14 +83,15 @@ def sync(
async def asyncio_detailed( async def asyncio_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Response[PushUser]: ) -> Response[MyWallet]:
""" """API for getting authenticated user balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[PushUser] Response[MyWallet]
""" """
kwargs = _get_kwargs() kwargs = _get_kwargs()
@ -102,14 +104,15 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
) -> Optional[PushUser]: ) -> Optional[MyWallet]:
""" """API for getting authenticated user balance.
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
PushUser MyWallet
""" """
return ( return (

View file

@ -0,0 +1,27 @@
"""Contains all the data models used in inputs/outputs"""
from .application_balance import ApplicationBalance
from .application_deposit import ApplicationDeposit
from .application_transaction import ApplicationTransaction
from .application_withdraw import ApplicationWithdraw
from .my_wallet import MyWallet
from .paginated_application_transaction_list import PaginatedApplicationTransactionList
from .paginated_user_transaction_list import PaginatedUserTransactionList
from .payee_type_enum import PayeeTypeEnum
from .payer_type_enum import PayerTypeEnum
from .state_enum import StateEnum
from .user_transaction import UserTransaction
__all__ = (
"ApplicationBalance",
"ApplicationDeposit",
"ApplicationTransaction",
"ApplicationWithdraw",
"MyWallet",
"PaginatedApplicationTransactionList",
"PaginatedUserTransactionList",
"PayeeTypeEnum",
"PayerTypeEnum",
"StateEnum",
"UserTransaction",
)

View file

@ -1,55 +1,45 @@
from collections.abc import Mapping from typing import Any, TypeVar, Union
from typing import Any, TypeVar
from uuid import UUID
from attrs import define as _attrs_define from attrs import define as _attrs_define
from attrs import field as _attrs_field from attrs import field as _attrs_field
T = TypeVar("T", bound="UserInquiry") from ..types import UNSET, Unset
T = TypeVar("T", bound="ApplicationBalance")
@_attrs_define @_attrs_define
class UserInquiry: class ApplicationBalance:
""" """
Attributes: Attributes:
uuid (UUID): balance (Union[Unset, int]):
phone_number (str):
""" """
uuid: UUID balance: Union[Unset, int] = UNSET
phone_number: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid) balance = self.balance
phone_number = self.phone_number
field_dict: dict[str, Any] = {} field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties) field_dict.update(self.additional_properties)
field_dict.update( field_dict.update({})
{ if balance is not UNSET:
"uuid": uuid, field_dict["balance"] = balance
"phone_number": phone_number,
}
)
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
d = dict(src_dict) d = src_dict.copy()
uuid = UUID(d.pop("uuid")) balance = d.pop("balance", UNSET)
phone_number = d.pop("phone_number") application_balance = cls(
balance=balance,
user_inquiry = cls(
uuid=uuid,
phone_number=phone_number,
) )
user_inquiry.additional_properties = d application_balance.additional_properties = d
return user_inquiry return application_balance
@property @property
def additional_keys(self) -> list[str]: def additional_keys(self) -> list[str]:

View file

@ -0,0 +1,235 @@
import datetime
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.payee_type_enum import PayeeTypeEnum
from ..models.payer_type_enum import PayerTypeEnum
from ..models.state_enum import StateEnum
from ..types import UNSET, Unset
T = TypeVar("T", bound="ApplicationDeposit")
@_attrs_define
class ApplicationDeposit:
"""
Attributes:
uuid (UUID):
payer_id (Union[None, UUID]):
payer_type (Union[None, PayerTypeEnum]):
payee_id (UUID):
created_at (Union[None, datetime.datetime]):
state (StateEnum): * `1` - created
* `2` - delayed
* `3` - pending
* `4` - incomplete
* `5` - success
* `6` - failed
* `7` - expected_failure
amount (int):
updated_at (Union[None, datetime.datetime]):
payee_type (Union[None, PayeeTypeEnum, Unset]):
details (Union[Unset, Any]):
"""
uuid: UUID
payer_id: Union[None, UUID]
payer_type: Union[None, PayerTypeEnum]
payee_id: UUID
created_at: Union[None, datetime.datetime]
state: StateEnum
amount: int
updated_at: Union[None, datetime.datetime]
payee_type: Union[None, PayeeTypeEnum, Unset] = UNSET
details: Union[Unset, Any] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
payer_id: Union[None, str]
if isinstance(self.payer_id, UUID):
payer_id = str(self.payer_id)
else:
payer_id = self.payer_id
payer_type: Union[None, int]
if isinstance(self.payer_type, PayerTypeEnum):
payer_type = self.payer_type.value
else:
payer_type = self.payer_type
payee_id = str(self.payee_id)
created_at: Union[None, str]
if isinstance(self.created_at, datetime.datetime):
created_at = self.created_at.isoformat()
else:
created_at = self.created_at
state = self.state.value
amount = self.amount
updated_at: Union[None, str]
if isinstance(self.updated_at, datetime.datetime):
updated_at = self.updated_at.isoformat()
else:
updated_at = self.updated_at
payee_type: Union[None, Unset, int]
if isinstance(self.payee_type, Unset):
payee_type = UNSET
elif isinstance(self.payee_type, PayeeTypeEnum):
payee_type = self.payee_type.value
else:
payee_type = self.payee_type
details = self.details
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"payer_id": payer_id,
"payer_type": payer_type,
"payee_id": payee_id,
"created_at": created_at,
"state": state,
"amount": amount,
"updated_at": updated_at,
}
)
if payee_type is not UNSET:
field_dict["payee_type"] = payee_type
if details is not UNSET:
field_dict["details"] = details
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
d = src_dict.copy()
uuid = UUID(d.pop("uuid"))
def _parse_payer_id(data: object) -> Union[None, UUID]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
payer_id_type_0 = UUID(data)
return payer_id_type_0
except: # noqa: E722
pass
return cast(Union[None, UUID], data)
payer_id = _parse_payer_id(d.pop("payer_id"))
def _parse_payer_type(data: object) -> Union[None, PayerTypeEnum]:
if data is None:
return data
try:
if not isinstance(data, int):
raise TypeError()
payer_type_type_0 = PayerTypeEnum(data)
return payer_type_type_0
except: # noqa: E722
pass
return cast(Union[None, PayerTypeEnum], data)
payer_type = _parse_payer_type(d.pop("payer_type"))
payee_id = UUID(d.pop("payee_id"))
def _parse_created_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
created_at_type_0 = isoparse(data)
return created_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
created_at = _parse_created_at(d.pop("created_at"))
state = StateEnum(d.pop("state"))
amount = d.pop("amount")
def _parse_updated_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
updated_at_type_0 = isoparse(data)
return updated_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
updated_at = _parse_updated_at(d.pop("updated_at"))
def _parse_payee_type(data: object) -> Union[None, PayeeTypeEnum, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, int):
raise TypeError()
payee_type_type_0 = PayeeTypeEnum(data)
return payee_type_type_0
except: # noqa: E722
pass
return cast(Union[None, PayeeTypeEnum, Unset], data)
payee_type = _parse_payee_type(d.pop("payee_type", UNSET))
details = d.pop("details", UNSET)
application_deposit = cls(
uuid=uuid,
payer_id=payer_id,
payer_type=payer_type,
payee_id=payee_id,
created_at=created_at,
state=state,
amount=amount,
updated_at=updated_at,
payee_type=payee_type,
details=details,
)
application_deposit.additional_properties = d
return application_deposit
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -0,0 +1,238 @@
import datetime
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.payer_type_enum import PayerTypeEnum
from ..models.state_enum import StateEnum
from ..types import UNSET, Unset
T = TypeVar("T", bound="ApplicationTransaction")
@_attrs_define
class ApplicationTransaction:
"""
Attributes:
uuid (UUID):
payee_id (UUID):
payee_type (UUID):
created_at (Union[None, datetime.datetime]):
state (StateEnum): * `1` - created
* `2` - delayed
* `3` - pending
* `4` - incomplete
* `5` - success
* `6` - failed
* `7` - expected_failure
amount (int):
updated_at (Union[None, datetime.datetime]):
details (Any):
role (str):
payer_id (Union[None, UUID, Unset]):
payer_type (Union[None, PayerTypeEnum, Unset]):
delay (Union[Unset, int]):
"""
uuid: UUID
payee_id: UUID
payee_type: UUID
created_at: Union[None, datetime.datetime]
state: StateEnum
amount: int
updated_at: Union[None, datetime.datetime]
details: Any
role: str
payer_id: Union[None, UUID, Unset] = UNSET
payer_type: Union[None, PayerTypeEnum, Unset] = UNSET
delay: Union[Unset, int] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
payee_id = str(self.payee_id)
payee_type = str(self.payee_type)
created_at: Union[None, str]
if isinstance(self.created_at, datetime.datetime):
created_at = self.created_at.isoformat()
else:
created_at = self.created_at
state = self.state.value
amount = self.amount
updated_at: Union[None, str]
if isinstance(self.updated_at, datetime.datetime):
updated_at = self.updated_at.isoformat()
else:
updated_at = self.updated_at
details = self.details
role = self.role
payer_id: Union[None, Unset, str]
if isinstance(self.payer_id, Unset):
payer_id = UNSET
elif isinstance(self.payer_id, UUID):
payer_id = str(self.payer_id)
else:
payer_id = self.payer_id
payer_type: Union[None, Unset, int]
if isinstance(self.payer_type, Unset):
payer_type = UNSET
elif isinstance(self.payer_type, PayerTypeEnum):
payer_type = self.payer_type.value
else:
payer_type = self.payer_type
delay = self.delay
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"payee_id": payee_id,
"payee_type": payee_type,
"created_at": created_at,
"state": state,
"amount": amount,
"updated_at": updated_at,
"details": details,
"role": role,
}
)
if payer_id is not UNSET:
field_dict["payer_id"] = payer_id
if payer_type is not UNSET:
field_dict["payer_type"] = payer_type
if delay is not UNSET:
field_dict["delay"] = delay
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
d = src_dict.copy()
uuid = UUID(d.pop("uuid"))
payee_id = UUID(d.pop("payee_id"))
payee_type = UUID(d.pop("payee_type"))
def _parse_created_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
created_at_type_0 = isoparse(data)
return created_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
created_at = _parse_created_at(d.pop("created_at"))
state = StateEnum(d.pop("state"))
amount = d.pop("amount")
def _parse_updated_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
updated_at_type_0 = isoparse(data)
return updated_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
updated_at = _parse_updated_at(d.pop("updated_at"))
details = d.pop("details")
role = d.pop("role")
def _parse_payer_id(data: object) -> Union[None, UUID, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
payer_id_type_0 = UUID(data)
return payer_id_type_0
except: # noqa: E722
pass
return cast(Union[None, UUID, Unset], data)
payer_id = _parse_payer_id(d.pop("payer_id", UNSET))
def _parse_payer_type(data: object) -> Union[None, PayerTypeEnum, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, int):
raise TypeError()
payer_type_type_0 = PayerTypeEnum(data)
return payer_type_type_0
except: # noqa: E722
pass
return cast(Union[None, PayerTypeEnum, Unset], data)
payer_type = _parse_payer_type(d.pop("payer_type", UNSET))
delay = d.pop("delay", UNSET)
application_transaction = cls(
uuid=uuid,
payee_id=payee_id,
payee_type=payee_type,
created_at=created_at,
state=state,
amount=amount,
updated_at=updated_at,
details=details,
role=role,
payer_id=payer_id,
payer_type=payer_type,
delay=delay,
)
application_transaction.additional_properties = d
return application_transaction
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -0,0 +1,239 @@
import datetime
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.payee_type_enum import PayeeTypeEnum
from ..models.payer_type_enum import PayerTypeEnum
from ..models.state_enum import StateEnum
from ..types import UNSET, Unset
T = TypeVar("T", bound="ApplicationWithdraw")
@_attrs_define
class ApplicationWithdraw:
"""
Attributes:
uuid (UUID):
payee_id (UUID):
payee_type (Union[None, PayeeTypeEnum]):
created_at (Union[None, datetime.datetime]):
state (StateEnum): * `1` - created
* `2` - delayed
* `3` - pending
* `4` - incomplete
* `5` - success
* `6` - failed
* `7` - expected_failure
amount (int):
updated_at (Union[None, datetime.datetime]):
details (Any):
payer_id (Union[None, UUID, Unset]):
payer_type (Union[None, PayerTypeEnum, Unset]):
"""
uuid: UUID
payee_id: UUID
payee_type: Union[None, PayeeTypeEnum]
created_at: Union[None, datetime.datetime]
state: StateEnum
amount: int
updated_at: Union[None, datetime.datetime]
details: Any
payer_id: Union[None, UUID, Unset] = UNSET
payer_type: Union[None, PayerTypeEnum, Unset] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
payee_id = str(self.payee_id)
payee_type: Union[None, int]
if isinstance(self.payee_type, PayeeTypeEnum):
payee_type = self.payee_type.value
else:
payee_type = self.payee_type
created_at: Union[None, str]
if isinstance(self.created_at, datetime.datetime):
created_at = self.created_at.isoformat()
else:
created_at = self.created_at
state = self.state.value
amount = self.amount
updated_at: Union[None, str]
if isinstance(self.updated_at, datetime.datetime):
updated_at = self.updated_at.isoformat()
else:
updated_at = self.updated_at
details = self.details
payer_id: Union[None, Unset, str]
if isinstance(self.payer_id, Unset):
payer_id = UNSET
elif isinstance(self.payer_id, UUID):
payer_id = str(self.payer_id)
else:
payer_id = self.payer_id
payer_type: Union[None, Unset, int]
if isinstance(self.payer_type, Unset):
payer_type = UNSET
elif isinstance(self.payer_type, PayerTypeEnum):
payer_type = self.payer_type.value
else:
payer_type = self.payer_type
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"payee_id": payee_id,
"payee_type": payee_type,
"created_at": created_at,
"state": state,
"amount": amount,
"updated_at": updated_at,
"details": details,
}
)
if payer_id is not UNSET:
field_dict["payer_id"] = payer_id
if payer_type is not UNSET:
field_dict["payer_type"] = payer_type
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
d = src_dict.copy()
uuid = UUID(d.pop("uuid"))
payee_id = UUID(d.pop("payee_id"))
def _parse_payee_type(data: object) -> Union[None, PayeeTypeEnum]:
if data is None:
return data
try:
if not isinstance(data, int):
raise TypeError()
payee_type_type_0 = PayeeTypeEnum(data)
return payee_type_type_0
except: # noqa: E722
pass
return cast(Union[None, PayeeTypeEnum], data)
payee_type = _parse_payee_type(d.pop("payee_type"))
def _parse_created_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
created_at_type_0 = isoparse(data)
return created_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
created_at = _parse_created_at(d.pop("created_at"))
state = StateEnum(d.pop("state"))
amount = d.pop("amount")
def _parse_updated_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
updated_at_type_0 = isoparse(data)
return updated_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
updated_at = _parse_updated_at(d.pop("updated_at"))
details = d.pop("details")
def _parse_payer_id(data: object) -> Union[None, UUID, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
payer_id_type_0 = UUID(data)
return payer_id_type_0
except: # noqa: E722
pass
return cast(Union[None, UUID, Unset], data)
payer_id = _parse_payer_id(d.pop("payer_id", UNSET))
def _parse_payer_type(data: object) -> Union[None, PayerTypeEnum, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, int):
raise TypeError()
payer_type_type_0 = PayerTypeEnum(data)
return payer_type_type_0
except: # noqa: E722
pass
return cast(Union[None, PayerTypeEnum, Unset], data)
payer_type = _parse_payer_type(d.pop("payer_type", UNSET))
application_withdraw = cls(
uuid=uuid,
payee_id=payee_id,
payee_type=payee_type,
created_at=created_at,
state=state,
amount=amount,
updated_at=updated_at,
details=details,
payer_id=payer_id,
payer_type=payer_type,
)
application_withdraw.additional_properties = d
return application_withdraw
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -0,0 +1,58 @@
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
T = TypeVar("T", bound="MyWallet")
@_attrs_define
class MyWallet:
"""
Attributes:
balance (Union[Unset, int]):
"""
balance: Union[Unset, int] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
balance = self.balance
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if balance is not UNSET:
field_dict["balance"] = balance
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
d = src_dict.copy()
balance = d.pop("balance", UNSET)
my_wallet = cls(
balance=balance,
)
my_wallet.additional_properties = d
return my_wallet
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,4 +1,3 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -7,24 +6,24 @@ from attrs import field as _attrs_field
from ..types import UNSET, Unset from ..types import UNSET, Unset
if TYPE_CHECKING: if TYPE_CHECKING:
from ..models.public_user import PublicUser from ..models.application_transaction import ApplicationTransaction
T = TypeVar("T", bound="PaginatedPublicUserList") T = TypeVar("T", bound="PaginatedApplicationTransactionList")
@_attrs_define @_attrs_define
class PaginatedPublicUserList: class PaginatedApplicationTransactionList:
""" """
Attributes: Attributes:
count (int): Example: 123. count (int): Example: 123.
results (list['PublicUser']): results (list['ApplicationTransaction']):
next_ (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=4. next_ (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=4.
previous (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=2. previous (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=2.
""" """
count: int count: int
results: list["PublicUser"] results: list["ApplicationTransaction"]
next_: Union[None, Unset, str] = UNSET next_: Union[None, Unset, str] = UNSET
previous: Union[None, Unset, str] = UNSET previous: Union[None, Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
@ -65,16 +64,16 @@ class PaginatedPublicUserList:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
from ..models.public_user import PublicUser from ..models.application_transaction import ApplicationTransaction
d = dict(src_dict) d = src_dict.copy()
count = d.pop("count") count = d.pop("count")
results = [] results = []
_results = d.pop("results") _results = d.pop("results")
for results_item_data in _results: for results_item_data in _results:
results_item = PublicUser.from_dict(results_item_data) results_item = ApplicationTransaction.from_dict(results_item_data)
results.append(results_item) results.append(results_item)
@ -96,15 +95,15 @@ class PaginatedPublicUserList:
previous = _parse_previous(d.pop("previous", UNSET)) previous = _parse_previous(d.pop("previous", UNSET))
paginated_public_user_list = cls( paginated_application_transaction_list = cls(
count=count, count=count,
results=results, results=results,
next_=next_, next_=next_,
previous=previous, previous=previous,
) )
paginated_public_user_list.additional_properties = d paginated_application_transaction_list.additional_properties = d
return paginated_public_user_list return paginated_application_transaction_list
@property @property
def additional_keys(self) -> list[str]: def additional_keys(self) -> list[str]:

View file

@ -1,4 +1,3 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeVar, Union, cast from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
from attrs import define as _attrs_define from attrs import define as _attrs_define
@ -7,24 +6,24 @@ from attrs import field as _attrs_field
from ..types import UNSET, Unset from ..types import UNSET, Unset
if TYPE_CHECKING: if TYPE_CHECKING:
from ..models.application import Application from ..models.user_transaction import UserTransaction
T = TypeVar("T", bound="PaginatedApplicationList") T = TypeVar("T", bound="PaginatedUserTransactionList")
@_attrs_define @_attrs_define
class PaginatedApplicationList: class PaginatedUserTransactionList:
""" """
Attributes: Attributes:
count (int): Example: 123. count (int): Example: 123.
results (list['Application']): results (list['UserTransaction']):
next_ (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=4. next_ (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=4.
previous (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=2. previous (Union[None, Unset, str]): Example: http://api.example.org/accounts/?page=2.
""" """
count: int count: int
results: list["Application"] results: list["UserTransaction"]
next_: Union[None, Unset, str] = UNSET next_: Union[None, Unset, str] = UNSET
previous: Union[None, Unset, str] = UNSET previous: Union[None, Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
@ -65,16 +64,16 @@ class PaginatedApplicationList:
return field_dict return field_dict
@classmethod @classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
from ..models.application import Application from ..models.user_transaction import UserTransaction
d = dict(src_dict) d = src_dict.copy()
count = d.pop("count") count = d.pop("count")
results = [] results = []
_results = d.pop("results") _results = d.pop("results")
for results_item_data in _results: for results_item_data in _results:
results_item = Application.from_dict(results_item_data) results_item = UserTransaction.from_dict(results_item_data)
results.append(results_item) results.append(results_item)
@ -96,15 +95,15 @@ class PaginatedApplicationList:
previous = _parse_previous(d.pop("previous", UNSET)) previous = _parse_previous(d.pop("previous", UNSET))
paginated_application_list = cls( paginated_user_transaction_list = cls(
count=count, count=count,
results=results, results=results,
next_=next_, next_=next_,
previous=previous, previous=previous,
) )
paginated_application_list.additional_properties = d paginated_user_transaction_list.additional_properties = d
return paginated_application_list return paginated_user_transaction_list
@property @property
def additional_keys(self) -> list[str]: def additional_keys(self) -> list[str]:

View file

@ -0,0 +1,9 @@
from enum import IntEnum
class PayeeTypeEnum(IntEnum):
VALUE_1 = 1
VALUE_2 = 2
def __str__(self) -> str:
return str(self.value)

View file

@ -0,0 +1,9 @@
from enum import IntEnum
class PayerTypeEnum(IntEnum):
VALUE_1 = 1
VALUE_2 = 2
def __str__(self) -> str:
return str(self.value)

View file

@ -0,0 +1,238 @@
import datetime
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.payer_type_enum import PayerTypeEnum
from ..models.state_enum import StateEnum
from ..types import UNSET, Unset
T = TypeVar("T", bound="UserTransaction")
@_attrs_define
class UserTransaction:
"""
Attributes:
uuid (UUID):
payee_id (UUID):
payee_type (UUID):
created_at (Union[None, datetime.datetime]):
state (StateEnum): * `1` - created
* `2` - delayed
* `3` - pending
* `4` - incomplete
* `5` - success
* `6` - failed
* `7` - expected_failure
amount (int):
updated_at (Union[None, datetime.datetime]):
details (Any):
role (int):
payer_id (Union[None, UUID, Unset]):
payer_type (Union[None, PayerTypeEnum, Unset]):
delay (Union[Unset, int]):
"""
uuid: UUID
payee_id: UUID
payee_type: UUID
created_at: Union[None, datetime.datetime]
state: StateEnum
amount: int
updated_at: Union[None, datetime.datetime]
details: Any
role: int
payer_id: Union[None, UUID, Unset] = UNSET
payer_type: Union[None, PayerTypeEnum, Unset] = UNSET
delay: Union[Unset, int] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
payee_id = str(self.payee_id)
payee_type = str(self.payee_type)
created_at: Union[None, str]
if isinstance(self.created_at, datetime.datetime):
created_at = self.created_at.isoformat()
else:
created_at = self.created_at
state = self.state.value
amount = self.amount
updated_at: Union[None, str]
if isinstance(self.updated_at, datetime.datetime):
updated_at = self.updated_at.isoformat()
else:
updated_at = self.updated_at
details = self.details
role = self.role
payer_id: Union[None, Unset, str]
if isinstance(self.payer_id, Unset):
payer_id = UNSET
elif isinstance(self.payer_id, UUID):
payer_id = str(self.payer_id)
else:
payer_id = self.payer_id
payer_type: Union[None, Unset, int]
if isinstance(self.payer_type, Unset):
payer_type = UNSET
elif isinstance(self.payer_type, PayerTypeEnum):
payer_type = self.payer_type.value
else:
payer_type = self.payer_type
delay = self.delay
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"payee_id": payee_id,
"payee_type": payee_type,
"created_at": created_at,
"state": state,
"amount": amount,
"updated_at": updated_at,
"details": details,
"role": role,
}
)
if payer_id is not UNSET:
field_dict["payer_id"] = payer_id
if payer_type is not UNSET:
field_dict["payer_type"] = payer_type
if delay is not UNSET:
field_dict["delay"] = delay
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: dict[str, Any]) -> T:
d = src_dict.copy()
uuid = UUID(d.pop("uuid"))
payee_id = UUID(d.pop("payee_id"))
payee_type = UUID(d.pop("payee_type"))
def _parse_created_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
created_at_type_0 = isoparse(data)
return created_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
created_at = _parse_created_at(d.pop("created_at"))
state = StateEnum(d.pop("state"))
amount = d.pop("amount")
def _parse_updated_at(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
updated_at_type_0 = isoparse(data)
return updated_at_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
updated_at = _parse_updated_at(d.pop("updated_at"))
details = d.pop("details")
role = d.pop("role")
def _parse_payer_id(data: object) -> Union[None, UUID, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
payer_id_type_0 = UUID(data)
return payer_id_type_0
except: # noqa: E722
pass
return cast(Union[None, UUID, Unset], data)
payer_id = _parse_payer_id(d.pop("payer_id", UNSET))
def _parse_payer_type(data: object) -> Union[None, PayerTypeEnum, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, int):
raise TypeError()
payer_type_type_0 = PayerTypeEnum(data)
return payer_type_type_0
except: # noqa: E722
pass
return cast(Union[None, PayerTypeEnum, Unset], data)
payer_type = _parse_payer_type(d.pop("payer_type", UNSET))
delay = d.pop("delay", UNSET)
user_transaction = cls(
uuid=uuid,
payee_id=payee_id,
payee_type=payee_type,
created_at=created_at,
state=state,
amount=amount,
updated_at=updated_at,
details=details,
role=role,
payer_id=payer_id,
payer_type=payer_type,
delay=delay,
)
user_transaction.additional_properties = d
return user_transaction
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -36,7 +36,6 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.postgres',
# pip installed apps # pip installed apps
'drf_spectacular', 'drf_spectacular',
@ -189,7 +188,7 @@ REST_FRAMEWORK = {
# 'otp_min': '1/min', # 'otp_min': '1/min',
# 'otp_day': '20/day', # 'otp_day': '20/day',
}, },
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler', 'EXCEPTION_HANDLER': 'utils.exceptions.ExceptionHandler',
} }
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
@ -232,7 +231,7 @@ SPECTACULAR_SETTINGS = {
AUTH_USER_MODEL = 'users.User' AUTH_USER_MODEL = 'users.User'
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_ALL_ORIGINS = True
CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*', 'http://*.gooyal.com', 'https://*.gooyal.com', 'http://*.gooyal.ir', 'https://*.gooyal.ir', ] CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*', 'http://*.gooyal.com', 'https://*.gooyal.com']
JALALI_DATE_DEFAULTS = { JALALI_DATE_DEFAULTS = {
'Strftime': { 'Strftime': {
@ -251,14 +250,20 @@ CACHES = {
} }
} }
LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'formatters': { 'formatters': {
# 'verbose': {
# 'format': '{levelname} AT: {asctime} LINE: {lineno} LOGGER: {name} FILE: {pathname} MESSAGE: {message}',
# 'style': '{',
# },
# 'simple': {
# 'format': '{levelname} {message}',
# 'style': '{',
# },
'loki': { 'loki': {
'class': 'utils.logs.LokiFormatter', # required 'class': "utils.logs.LokiFormatter", # required
}, },
}, },
@ -268,23 +273,32 @@ LOGGING = {
'class': 'utils.logs.LokiHandler', # Required 'class': 'utils.logs.LokiHandler', # Required
'formatter': 'loki', # Loki formatter. Required 'formatter': 'loki', # Loki formatter. Required
'timeout': 1, # Post request timeout, default is 0.5. Optional 'timeout': 1, # Post request timeout, default is 0.5. Optional
'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional. 'url': 'https://loki.winsoo.ir/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
# 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
'tags': {"app": "promotions", "env": "production"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use. 'tags': {"app": "ads", "env": "production"},
'mode': 'thread', # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional. # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
}, 'mode': 'thread',
'console': { # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
'level': 'DEBUG',
'class': 'logging.StreamHandler',
# 'formatter': 'verbose',
}, },
# 'file': {
# 'level': 'DEBUG',
# 'class': 'logging.handlers.TimedRotatingFileHandler',
# 'filename': 'logs/app.log',
# 'formatter': 'verbose',
# "when": 'D',
# "backupCount": 5,
# },
}, },
'loggers': { 'loggers': {
'': { 'django': {
'handlers': ['console', 'loki'], 'handlers': ['loki'],
'level': 'INFO', 'level': 'INFO',
'propagate': True,
}, },
# 'root': {
# 'handlers': ['file'],
# 'level': 'DEBUG',
# 'propagate': True,
# },
}, },
} }
@ -294,30 +308,6 @@ from typing import List, Tuple
STORAGES = { STORAGES = {
"default": { "default": {
"BACKEND": "django_minio_backend.models.MinioBackend", "BACKEND": "django_minio_backend.models.MinioBackend",
"OPTIONS": {
"MINIO_ENDPOINT": config('MINIO_ENDPOINT', default='drive.gooyal.com'),
"MINIO_USE_HTTPS": config('MINIO_USE_HTTPS', default=True, cast=bool),
"MINIO_EXTERNAL_ENDPOINT": config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.com'), # Default is same as MINIO_ENDPOINT
"MINIO_EXTERNAL_ENDPOINT_USE_HTTPS": config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool), # Default is same as MINIO_USE_HTTPS
"MINIO_REGION": None, # Default is set to None
"MINIO_ACCESS_KEY": config('MINIO_ACCESS_KEY'),
"MINIO_SECRET_KEY": config('MINIO_SECRET_KEY'),
"MINIO_URL_EXPIRY_HOURS": timedelta(days=1), # Default is 7 days (longest) if not defined
"MINIO_CONSISTENCY_CHECK_ON_START": True,
"MINIO_DEFAULT_BUCKET": config('MINIO_MEDIA_FILES_BUCKET'), # replacement for MEDIA_ROOT
# MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
"MINIO_PRIVATE_BUCKETS": [
'default',
],
"MINIO_PUBLIC_BUCKETS": [
# 'default-public',
config('MINIO_MEDIA_FILES_BUCKET')
],
"MINIO_POLICY_HOOKS": [],
"MINIO_BUCKET_CHECK_ON_SAVE": True # Default: True // Creates bucket if missing, then save
},
}, },
"staticfiles": { "staticfiles": {
# "BACKEND": "django_minio_backend.models.MinioBackendStatic", # "BACKEND": "django_minio_backend.models.MinioBackendStatic",
@ -337,7 +327,7 @@ MINIO_REGION = None # Default is set to None
MINIO_ACCESS_KEY = config('MINIO_ACCESS_KEY') MINIO_ACCESS_KEY = config('MINIO_ACCESS_KEY')
MINIO_SECRET_KEY = config('MINIO_SECRET_KEY') MINIO_SECRET_KEY = config('MINIO_SECRET_KEY')
MINIO_URL_EXPIRY_HOURS = timedelta(days=1) # Default is 7 days (longest) if not defined MINIO_URL_EXPIRY_HOURS = timedelta(days=1) # Default is 7 days (longest) if not defined
MINIO_CONSISTENCY_CHECK_ON_START = True MINIO_CONSISTENCY_CHECK_ON_START = False
MINIO_MEDIA_FILES_BUCKET = config('MINIO_MEDIA_FILES_BUCKET') # replacement for MEDIA_ROOT MINIO_MEDIA_FILES_BUCKET = config('MINIO_MEDIA_FILES_BUCKET') # replacement for MEDIA_ROOT
# MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT # MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
@ -381,18 +371,3 @@ CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_BROKER_URL = REDIS_BASE_URL CELERY_BROKER_URL = REDIS_BASE_URL
CELERY_RESULT_BACKEND = REDIS_BASE_URL CELERY_RESULT_BACKEND = REDIS_BASE_URL
ACCOUNTS_BASE_PUBLIC_URL = config('ACCOUNTS_BASE_PUBLIC_URL', default=None, cast=str)
WALLET_BASE_PUBLIC_URL = config('WALLET_BASE_PUBLIC_URL', default=None, cast=str)
WALLET_RIAL_DEPOSIT = config('WALLET_RIAL_DEPOSIT', cast=str)
WALLET_USER_BILLBOARD_VISIT_INCOME = config('WALLET_USER_BILLBOARD_VISIT_INCOME', cast=str)
# Company-side pool promotion payouts are drawn from; must be distinct from the user-side
# wallet above. TODO(you): replace with the real wallet-type UUID from the wallet service.
WALLET_PROMOTIONS_CREDIT = config('WALLET_PROMOTIONS_CREDIT', cast=str)
# Same wallet-service UUID as the advertising repo's own WALLET_ADVERTISING_TRANSIT setting.
# Destination for a Recipient whose wallet_destination is advertising_transit — billboard/ad
# credit rather than a cash-like user reward — see Recipient.get_wallet_category_uuid().
WALLET_ADVERTISING_TRANSIT = config('WALLET_ADVERTISING_TRANSIT', cast=str)
NOTIFICATIONS_BASE_PUBLIC_URL = config('NOTIFICATIONS_BASE_PUBLIC_URL', default=None, cast=str)

View file

@ -20,6 +20,7 @@ from django.urls import path, include
from django.contrib import admin from django.contrib import admin
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [ urlpatterns = [
path('swagger/', SpectacularAPIView.as_view(), name='schema'), path('swagger/', SpectacularAPIView.as_view(), name='schema'),
path('swagger/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('swagger/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
@ -28,8 +29,7 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', include('utils.urls')), path('', include('utils.urls')),
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
path('promotions/', include('apps.promotions.urls_user', namespace='promotions')), path('promotions/', include('apps.promotions.urls', namespace='promotions')),
path('api/v2/promotions/application/<user_uuid>/', include('apps.promotions.urls_application', namespace='promotions-application')),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,42 +1,44 @@
# #
# This file is autogenerated by pip-compile with Python 3.13 # This file is autogenerated by pip-compile with Python 3.11
# by the following command: # by the following command:
# #
# pip-compile # pip-compile
# #
amqp==5.3.1 amqp==5.3.1
# via kombu # via kombu
anyio==4.12.1 anyio==4.9.0
# via httpx # via httpx
argon2-cffi==25.1.0 argon2-cffi==25.1.0
# via minio # via minio
argon2-cffi-bindings==25.1.0 argon2-cffi-bindings==21.2.0
# via argon2-cffi # via argon2-cffi
asgiref==3.11.1 asgiref==3.9.1
# via # via
# django # django
# django-cors-headers # django-cors-headers
attrs==25.4.0 async-timeout==5.0.1
# via redis
attrs==25.3.0
# via # via
# jsonschema # jsonschema
# referencing # referencing
billiard==4.2.4 billiard==4.2.1
# via celery # via celery
celery==5.6.2 celery==5.5.3
# via -r requirements.in # via -r requirements.in
certifi==2026.1.4 certifi==2025.7.14
# via # via
# httpcore # httpcore
# httpx # httpx
# minio # minio
# requests # requests
cffi==2.0.0 cffi==1.17.1
# via # via
# argon2-cffi-bindings # argon2-cffi-bindings
# cryptography # cryptography
charset-normalizer==3.4.4 charset-normalizer==3.4.2
# via requests # via requests
click==8.3.1 click==8.2.1
# via # via
# celery # celery
# click-didyoumean # click-didyoumean
@ -50,9 +52,9 @@ click-repl==0.3.0
# via celery # via celery
crispy-bootstrap5==2025.6 crispy-bootstrap5==2025.6
# via -r requirements.in # via -r requirements.in
cryptography==46.0.5 cryptography==45.0.5
# via jwcrypto # via jwcrypto
django==6.0.2 django==5.2.4
# via # via
# -r requirements.in # -r requirements.in
# crispy-bootstrap5 # crispy-bootstrap5
@ -65,33 +67,33 @@ django==6.0.2
# django-redis # django-redis
# djangorestframework # djangorestframework
# drf-spectacular # drf-spectacular
django-cors-headers==4.9.0 django-cors-headers==4.7.0
# via -r requirements.in # via -r requirements.in
django-crispy-forms==2.5 django-crispy-forms==2.4
# via # via
# -r requirements.in # -r requirements.in
# crispy-bootstrap5 # crispy-bootstrap5
django-filter==25.2 django-filter==25.1
# via -r requirements.in # via -r requirements.in
django-jalali-date==2.0.0 django-jalali-date==2.0.0
# via -r requirements.in # via -r requirements.in
django-minio-backend==4.5.0 django-minio-backend==3.8.0
# via -r requirements.in # via -r requirements.in
django-oauth-toolkit==3.2.0 django-oauth-toolkit==3.0.1
# via -r requirements.in # via -r requirements.in
django-redis==6.0.0 django-redis==6.0.0
# via -r requirements.in # via -r requirements.in
djangorestframework==3.16.1 djangorestframework==3.16.0
# via # via
# -r requirements.in # -r requirements.in
# drf-spectacular # drf-spectacular
drf-spectacular==0.29.0 drf-spectacular==0.28.0
# via -r requirements.in # via -r requirements.in
gevent==25.9.1 gevent==25.5.1
# via -r requirements.in # via -r requirements.in
greenlet==3.3.2 greenlet==3.2.3
# via gevent # via gevent
gunicorn==25.1.0 gunicorn==23.0.0
# via -r requirements.in # via -r requirements.in
h11==0.16.0 h11==0.16.0
# via httpcore # via httpcore
@ -99,7 +101,7 @@ httpcore==1.0.9
# via httpx # via httpx
httpx==0.28.1 httpx==0.28.1
# via -r requirements.in # via -r requirements.in
idna==3.11 idna==3.10
# via # via
# anyio # anyio
# httpx # httpx
@ -110,35 +112,33 @@ jalali-core==1.0.0
# via jdatetime # via jdatetime
jdatetime==5.2.0 jdatetime==5.2.0
# via django-jalali-date # via django-jalali-date
jsonschema==4.26.0 jsonschema==4.25.0
# via drf-spectacular # via drf-spectacular
jsonschema-specifications==2025.9.1 jsonschema-specifications==2025.4.1
# via jsonschema # via jsonschema
jwcrypto==1.5.6 jwcrypto==1.5.6
# via django-oauth-toolkit # via django-oauth-toolkit
kombu==5.6.2 kombu==5.5.4
# via celery # via celery
minio==7.2.20 minio==7.2.16
# via django-minio-backend # via django-minio-backend
oauthlib==3.3.1 oauthlib==3.3.1
# via django-oauth-toolkit # via django-oauth-toolkit
packaging==26.0 packaging==25.0
# via # via
# gunicorn # gunicorn
# kombu # kombu
pillow==12.1.1 pillow==11.3.0
# via
# -r requirements.in
# django-minio-backend
prompt-toolkit==3.0.52
# via click-repl
psycopg[binary,pool]==3.3.3
# via -r requirements.in # via -r requirements.in
psycopg-binary==3.3.3 prompt-toolkit==3.0.51
# via click-repl
psycopg[binary,pool]==3.2.9
# via -r requirements.in
psycopg-binary==3.2.9
# via psycopg # via psycopg
psycopg-pool==3.3.0 psycopg-pool==3.2.6
# via psycopg # via psycopg
pycparser==3.0 pycparser==2.22
# via cffi # via cffi
pycryptodome==3.23.0 pycryptodome==3.23.0
# via minio # via minio
@ -146,40 +146,43 @@ python-dateutil==2.9.0.post0
# via celery # via celery
python-decouple==3.8 python-decouple==3.8
# via -r requirements.in # via -r requirements.in
pyyaml==6.0.3 pyyaml==6.0.2
# via drf-spectacular # via drf-spectacular
redis==7.2.0 redis==6.2.0
# via # via
# -r requirements.in # -r requirements.in
# django-redis # django-redis
referencing==0.37.0 referencing==0.36.2
# via # via
# jsonschema # jsonschema
# jsonschema-specifications # jsonschema-specifications
requests==2.32.5 requests==2.32.4
# via # via
# -r requirements.in # -r requirements.in
# django-oauth-toolkit # django-oauth-toolkit
rpds-py==0.30.0 rpds-py==0.26.0
# via # via
# jsonschema # jsonschema
# referencing # referencing
six==1.17.0 six==1.17.0
# via python-dateutil # via python-dateutil
sqlparse==0.5.5 sniffio==1.3.1
# via anyio
sqlparse==0.5.3
# via django # via django
typing-extensions==4.15.0 typing-extensions==4.14.1
# via # via
# anyio
# jwcrypto # jwcrypto
# minio # minio
# psycopg
# psycopg-pool # psycopg-pool
tzdata==2025.3 # referencing
tzdata==2025.2
# via kombu # via kombu
tzlocal==5.3.1
# via celery
uritemplate==4.2.0 uritemplate==4.2.0
# via drf-spectacular # via drf-spectacular
urllib3==2.6.3 urllib3==2.5.0
# via # via
# minio # minio
# requests # requests
@ -188,9 +191,12 @@ vine==5.1.0
# amqp # amqp
# celery # celery
# kombu # kombu
wcwidth==0.6.0 wcwidth==0.2.13
# via prompt-toolkit # via prompt-toolkit
zope-event==6.1 zope-event==5.1.1
# via gevent # via gevent
zope-interface==8.2 zope-interface==7.2
# via gevent # via gevent
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View file

@ -1,89 +0,0 @@
import time
import requests
from django.conf import settings
from django.core.cache import cache
from httpx import Request
from utils.clients.gooyal_accounts_client import AuthenticatedClient
from utils.clients.gooyal_accounts_client.models import PublicUser, Application
from utils.clients.gooyal_accounts_client.api.users import users_api_users_retrieve
from utils.clients.gooyal_accounts_client.api.oauth2 import oauth2_apis_applications_retrieve
from utils.clients.gooyal_accounts_client.models import Account
from utils.clients.gooyal_accounts_client.api.users import (users_api_users_details_retrieve)
def login_as_client_credentials():
access_token = cache.get('promotions_access_token')
# expires_in = self.auth_data.get('expires_in')
# created_at = self.auth_data.get('created_at')
#
# if access_token and expires_in > time.time() - created_at:
# return self.auth_data
if access_token:
print('already in cache')
return access_token
print('not in cache')
data = {
"grant_type": 'client_credentials',
"scope": settings.OAUTH2_SCOPES,
}
auth = (settings.OAUTH2_CLIENT_ID, settings.OAUTH2_CLIENT_SECRET)
response = requests.post(f'{settings.OAUTH2_PROVIDER_PUBLIC_URL}/token/',
data=data,
auth=auth)
auth_data = response.json()
print(auth_data)
if 'access_token' in auth_data:
auth_data['created_at'] = time.time()
cache.set('promotions_access_token', auth_data, timeout=auth_data.get('expires_in') - 5)
return auth_data
def log_request(request: Request):
print(request.headers['authorization'])
print(
f"Request event hook: {request.method} {request.url} {request.method} {request.headers} - Waiting for response")
def log_response(response):
request = response.request
print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
def get_client():
access_token = login_as_client_credentials()['access_token']
client = AuthenticatedClient(base_url=settings.ACCOUNTS_BASE_PUBLIC_URL, token=access_token,
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}
)
return client
def get_user_info(uuid):
client = get_client()
response: PublicUser = users_api_users_retrieve.sync(client=client, uuid=uuid)
print(response)
return response
def get_user_detailed_info(uuid):
client = get_client()
response: Account = users_api_users_details_retrieve.sync(uuid=uuid, client=client)
print(response)
return response
def get_application_details(uuid):
client = get_client()
data: Application = oauth2_apis_applications_retrieve.sync(client=client, id=uuid)
print(data)
return data

View file

@ -1,8 +0,0 @@
"""A client library for accessing Gooyal Accounts"""
from .client import AuthenticatedClient, Client
__all__ = (
"AuthenticatedClient",
"Client",
)

View file

@ -1 +0,0 @@
"""Contains endpoint functions for accessing the API"""

View file

@ -1,142 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.application import Application
from ...types import Response
def _get_kwargs(
id: str,
) -> dict[str, Any]:
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/oauth2/apis/applications/{id}/avatar/",
}
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Application]:
if response.status_code == 200:
response_200 = Application.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Application]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
id: str,
*,
client: AuthenticatedClient,
) -> Response[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Application]
"""
kwargs = _get_kwargs(
id=id,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
id: str,
*,
client: AuthenticatedClient,
) -> Optional[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Application
"""
return sync_detailed(
id=id,
client=client,
).parsed
async def asyncio_detailed(
id: str,
*,
client: AuthenticatedClient,
) -> Response[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Application]
"""
kwargs = _get_kwargs(
id=id,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
id: str,
*,
client: AuthenticatedClient,
) -> Optional[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Application
"""
return (
await asyncio_detailed(
id=id,
client=client,
)
).parsed

View file

@ -1,142 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.application import Application
from ...types import Response
def _get_kwargs(
id: str,
) -> dict[str, Any]:
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/oauth2/apis/applications/{id}/",
}
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Application]:
if response.status_code == 200:
response_200 = Application.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Application]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
id: str,
*,
client: AuthenticatedClient,
) -> Response[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Application]
"""
kwargs = _get_kwargs(
id=id,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
id: str,
*,
client: AuthenticatedClient,
) -> Optional[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Application
"""
return sync_detailed(
id=id,
client=client,
).parsed
async def asyncio_detailed(
id: str,
*,
client: AuthenticatedClient,
) -> Response[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Application]
"""
kwargs = _get_kwargs(
id=id,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
id: str,
*,
client: AuthenticatedClient,
) -> Optional[Application]:
"""
Args:
id (str):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Application
"""
return (
await asyncio_detailed(
id=id,
client=client,
)
).parsed

View file

@ -1 +0,0 @@
"""Contains endpoint functions for accessing the API"""

View file

@ -1,152 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.account import Account
from ...models.patched_account import PatchedAccount
from ...types import Response
def _get_kwargs(
*,
body: PatchedAccount,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "patch",
"url": "/users/api/account/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Account]:
if response.status_code == 200:
response_200 = Account.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Account]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: PatchedAccount,
) -> Response[Account]:
"""
Args:
body (PatchedAccount):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: PatchedAccount,
) -> Optional[Account]:
"""
Args:
body (PatchedAccount):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: PatchedAccount,
) -> Response[Account]:
"""
Args:
body (PatchedAccount):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: PatchedAccount,
) -> Optional[Account]:
"""
Args:
body (PatchedAccount):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,151 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.account import Account
from ...types import Response
def _get_kwargs(
*,
body: Account,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "put",
"url": "/users/api/account/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Account]:
if response.status_code == 200:
response_200 = Account.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Account]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: Account,
) -> Response[Account]:
"""
Args:
body (Account):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: Account,
) -> Optional[Account]:
"""
Args:
body (Account):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: Account,
) -> Response[Account]:
"""
Args:
body (Account):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: Account,
) -> Optional[Account]:
"""
Args:
body (Account):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,156 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.change_password import ChangePassword
from ...models.patched_change_password import PatchedChangePassword
from ...types import Response
def _get_kwargs(
*,
body: PatchedChangePassword,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "patch",
"url": "/users/api/change_password/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ChangePassword]:
if response.status_code == 200:
response_200 = ChangePassword.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ChangePassword]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: PatchedChangePassword,
) -> Response[ChangePassword]:
"""
Args:
body (PatchedChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ChangePassword]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: PatchedChangePassword,
) -> Optional[ChangePassword]:
"""
Args:
body (PatchedChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ChangePassword
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: PatchedChangePassword,
) -> Response[ChangePassword]:
"""
Args:
body (PatchedChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ChangePassword]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: PatchedChangePassword,
) -> Optional[ChangePassword]:
"""
Args:
body (PatchedChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ChangePassword
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,155 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.change_password import ChangePassword
from ...types import Response
def _get_kwargs(
*,
body: ChangePassword,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "put",
"url": "/users/api/change_password/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ChangePassword]:
if response.status_code == 200:
response_200 = ChangePassword.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ChangePassword]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: ChangePassword,
) -> Response[ChangePassword]:
"""
Args:
body (ChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ChangePassword]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: ChangePassword,
) -> Optional[ChangePassword]:
"""
Args:
body (ChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ChangePassword
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: ChangePassword,
) -> Response[ChangePassword]:
"""
Args:
body (ChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ChangePassword]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: ChangePassword,
) -> Optional[ChangePassword]:
"""
Args:
body (ChangePassword):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ChangePassword
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,151 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.user_inquiry import UserInquiry
from ...types import Response
def _get_kwargs(
*,
body: UserInquiry,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": "/users/api/inquiry/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[UserInquiry]:
if response.status_code == 201:
response_201 = UserInquiry.from_dict(response.json())
return response_201
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[UserInquiry]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Response[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[UserInquiry]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Optional[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
UserInquiry
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Response[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[UserInquiry]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: UserInquiry,
) -> Optional[UserInquiry]:
"""
Args:
body (UserInquiry):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
UserInquiry
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,151 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.request_otp import RequestOTP
from ...types import Response
def _get_kwargs(
*,
body: RequestOTP,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": "/users/api/request_otp/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[RequestOTP]:
if response.status_code == 201:
response_201 = RequestOTP.from_dict(response.json())
return response_201
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[RequestOTP]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: RequestOTP,
) -> Response[RequestOTP]:
"""
Args:
body (RequestOTP):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[RequestOTP]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: RequestOTP,
) -> Optional[RequestOTP]:
"""
Args:
body (RequestOTP):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
RequestOTP
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: RequestOTP,
) -> Response[RequestOTP]:
"""
Args:
body (RequestOTP):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[RequestOTP]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: RequestOTP,
) -> Optional[RequestOTP]:
"""
Args:
body (RequestOTP):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
RequestOTP
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,151 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.request_ott import RequestOTT
from ...types import Response
def _get_kwargs(
*,
body: RequestOTT,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": "/users/api/request_ott/",
}
_body = body.to_dict()
_kwargs["json"] = _body
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[RequestOTT]:
if response.status_code == 201:
response_201 = RequestOTT.from_dict(response.json())
return response_201
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[RequestOTT]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
body: RequestOTT,
) -> Response[RequestOTT]:
"""
Args:
body (RequestOTT):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[RequestOTT]
"""
kwargs = _get_kwargs(
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
body: RequestOTT,
) -> Optional[RequestOTT]:
"""
Args:
body (RequestOTT):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
RequestOTT
"""
return sync_detailed(
client=client,
body=body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
body: RequestOTT,
) -> Response[RequestOTT]:
"""
Args:
body (RequestOTT):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[RequestOTT]
"""
kwargs = _get_kwargs(
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
body: RequestOTT,
) -> Optional[RequestOTT]:
"""
Args:
body (RequestOTT):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
RequestOTT
"""
return (
await asyncio_detailed(
client=client,
body=body,
)
).parsed

View file

@ -1,92 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
from uuid import UUID
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...types import Response
def _get_kwargs(
uuid: UUID,
) -> dict[str, Any]:
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/users/api/users/{uuid}/avatar",
}
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
if response.status_code == 200:
return None
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Response[Any]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Any]
"""
kwargs = _get_kwargs(
uuid=uuid,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
async def asyncio_detailed(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Response[Any]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Any]
"""
kwargs = _get_kwargs(
uuid=uuid,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)

View file

@ -1,143 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
from uuid import UUID
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.account import Account
from ...types import Response
def _get_kwargs(
uuid: UUID,
) -> dict[str, Any]:
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/users/api/users/{uuid}/details",
}
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Account]:
if response.status_code == 200:
response_200 = Account.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Account]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Response[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
uuid=uuid,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Optional[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return sync_detailed(
uuid=uuid,
client=client,
).parsed
async def asyncio_detailed(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Response[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Account]
"""
kwargs = _get_kwargs(
uuid=uuid,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
uuid: UUID,
*,
client: AuthenticatedClient,
) -> Optional[Account]:
"""
Args:
uuid (UUID):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Account
"""
return (
await asyncio_detailed(
uuid=uuid,
client=client,
)
).parsed

View file

@ -1,33 +0,0 @@
"""Contains all the data models used in inputs/outputs"""
from .account import Account
from .application import Application
from .blank_enum import BlankEnum
from .change_password import ChangePassword
from .education_degree_enum import EducationDegreeEnum
from .gender_enum import GenderEnum
from .paginated_application_list import PaginatedApplicationList
from .paginated_public_user_list import PaginatedPublicUserList
from .patched_account import PatchedAccount
from .patched_change_password import PatchedChangePassword
from .public_user import PublicUser
from .request_otp import RequestOTP
from .request_ott import RequestOTT
from .user_inquiry import UserInquiry
__all__ = (
"Account",
"Application",
"BlankEnum",
"ChangePassword",
"EducationDegreeEnum",
"GenderEnum",
"PaginatedApplicationList",
"PaginatedPublicUserList",
"PatchedAccount",
"PatchedChangePassword",
"PublicUser",
"RequestOTP",
"RequestOTT",
"UserInquiry",
)

View file

@ -1,410 +0,0 @@
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.blank_enum import BlankEnum
from ..models.education_degree_enum import EducationDegreeEnum
from ..models.gender_enum import GenderEnum
from ..types import UNSET, Unset
T = TypeVar("T", bound="Account")
@_attrs_define
class Account:
"""
Attributes:
uuid (UUID):
email (str):
phone_number (str):
iban_verified (Union[None, bool]):
province_name (str):
city_name (str):
username (Union[None, Unset, str]): Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
first_name (Union[Unset, str]):
last_name (Union[Unset, str]):
name (Union[Unset, str]):
avatar (Union[None, Unset, str]):
gender (Union[BlankEnum, GenderEnum, None, Unset]):
education_degree (Union[BlankEnum, EducationDegreeEnum, None, Unset]):
education_field (Union[None, Unset, str]):
birthdate (Union[None, Unset, datetime.date]):
iban (Union[None, Unset, str]):
province (Union[None, Unset, int]):
city (Union[None, Unset, int]):
postal_code (Union[None, Unset, str]):
address (Union[None, Unset, str]):
"""
uuid: UUID
email: str
phone_number: str
iban_verified: Union[None, bool]
province_name: str
city_name: str
username: Union[None, Unset, str] = UNSET
first_name: Union[Unset, str] = UNSET
last_name: Union[Unset, str] = UNSET
name: Union[Unset, str] = UNSET
avatar: Union[None, Unset, str] = UNSET
gender: Union[BlankEnum, GenderEnum, None, Unset] = UNSET
education_degree: Union[BlankEnum, EducationDegreeEnum, None, Unset] = UNSET
education_field: Union[None, Unset, str] = UNSET
birthdate: Union[None, Unset, datetime.date] = UNSET
iban: Union[None, Unset, str] = UNSET
province: Union[None, Unset, int] = UNSET
city: Union[None, Unset, int] = UNSET
postal_code: Union[None, Unset, str] = UNSET
address: Union[None, Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
email = self.email
phone_number = self.phone_number
iban_verified: Union[None, bool]
iban_verified = self.iban_verified
province_name = self.province_name
city_name = self.city_name
username: Union[None, Unset, str]
if isinstance(self.username, Unset):
username = UNSET
else:
username = self.username
first_name = self.first_name
last_name = self.last_name
name = self.name
avatar: Union[None, Unset, str]
if isinstance(self.avatar, Unset):
avatar = UNSET
else:
avatar = self.avatar
gender: Union[None, Unset, str]
if isinstance(self.gender, Unset):
gender = UNSET
elif isinstance(self.gender, GenderEnum):
gender = self.gender.value
elif isinstance(self.gender, BlankEnum):
gender = self.gender.value
else:
gender = self.gender
education_degree: Union[None, Unset, str]
if isinstance(self.education_degree, Unset):
education_degree = UNSET
elif isinstance(self.education_degree, EducationDegreeEnum):
education_degree = self.education_degree.value
elif isinstance(self.education_degree, BlankEnum):
education_degree = self.education_degree.value
else:
education_degree = self.education_degree
education_field: Union[None, Unset, str]
if isinstance(self.education_field, Unset):
education_field = UNSET
else:
education_field = self.education_field
birthdate: Union[None, Unset, str]
if isinstance(self.birthdate, Unset):
birthdate = UNSET
elif isinstance(self.birthdate, datetime.date):
birthdate = self.birthdate.isoformat()
else:
birthdate = self.birthdate
iban: Union[None, Unset, str]
if isinstance(self.iban, Unset):
iban = UNSET
else:
iban = self.iban
province: Union[None, Unset, int]
if isinstance(self.province, Unset):
province = UNSET
else:
province = self.province
city: Union[None, Unset, int]
if isinstance(self.city, Unset):
city = UNSET
else:
city = self.city
postal_code: Union[None, Unset, str]
if isinstance(self.postal_code, Unset):
postal_code = UNSET
else:
postal_code = self.postal_code
address: Union[None, Unset, str]
if isinstance(self.address, Unset):
address = UNSET
else:
address = self.address
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"email": email,
"phone_number": phone_number,
"iban_verified": iban_verified,
"province_name": province_name,
"city_name": city_name,
}
)
if username is not UNSET:
field_dict["username"] = username
if first_name is not UNSET:
field_dict["first_name"] = first_name
if last_name is not UNSET:
field_dict["last_name"] = last_name
if name is not UNSET:
field_dict["name"] = name
if avatar is not UNSET:
field_dict["avatar"] = avatar
if gender is not UNSET:
field_dict["gender"] = gender
if education_degree is not UNSET:
field_dict["education_degree"] = education_degree
if education_field is not UNSET:
field_dict["education_field"] = education_field
if birthdate is not UNSET:
field_dict["birthdate"] = birthdate
if iban is not UNSET:
field_dict["iban"] = iban
if province is not UNSET:
field_dict["province"] = province
if city is not UNSET:
field_dict["city"] = city
if postal_code is not UNSET:
field_dict["postal_code"] = postal_code
if address is not UNSET:
field_dict["address"] = address
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
uuid = UUID(d.pop("uuid"))
email = d.pop("email")
phone_number = d.pop("phone_number")
def _parse_iban_verified(data: object) -> Union[None, bool]:
if data is None:
return data
return cast(Union[None, bool], data)
iban_verified = _parse_iban_verified(d.pop("iban_verified"))
province_name = d.pop("province_name")
city_name = d.pop("city_name")
def _parse_username(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
username = _parse_username(d.pop("username", UNSET))
first_name = d.pop("first_name", UNSET)
last_name = d.pop("last_name", UNSET)
name = d.pop("name", UNSET)
def _parse_avatar(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
avatar = _parse_avatar(d.pop("avatar", UNSET))
def _parse_gender(data: object) -> Union[BlankEnum, GenderEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
gender_type_0 = GenderEnum(data)
return gender_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
gender_type_1 = BlankEnum(data)
return gender_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, GenderEnum, None, Unset], data)
gender = _parse_gender(d.pop("gender", UNSET))
def _parse_education_degree(data: object) -> Union[BlankEnum, EducationDegreeEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_0 = EducationDegreeEnum(data)
return education_degree_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_1 = BlankEnum(data)
return education_degree_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, EducationDegreeEnum, None, Unset], data)
education_degree = _parse_education_degree(d.pop("education_degree", UNSET))
def _parse_education_field(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
education_field = _parse_education_field(d.pop("education_field", UNSET))
def _parse_birthdate(data: object) -> Union[None, Unset, datetime.date]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
birthdate_type_0 = isoparse(data).date()
return birthdate_type_0
except: # noqa: E722
pass
return cast(Union[None, Unset, datetime.date], data)
birthdate = _parse_birthdate(d.pop("birthdate", UNSET))
def _parse_iban(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
iban = _parse_iban(d.pop("iban", UNSET))
def _parse_province(data: object) -> Union[None, Unset, int]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, int], data)
province = _parse_province(d.pop("province", UNSET))
def _parse_city(data: object) -> Union[None, Unset, int]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, int], data)
city = _parse_city(d.pop("city", UNSET))
def _parse_postal_code(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
postal_code = _parse_postal_code(d.pop("postal_code", UNSET))
def _parse_address(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
address = _parse_address(d.pop("address", UNSET))
account = cls(
uuid=uuid,
email=email,
phone_number=phone_number,
iban_verified=iban_verified,
province_name=province_name,
city_name=city_name,
username=username,
first_name=first_name,
last_name=last_name,
name=name,
avatar=avatar,
gender=gender,
education_degree=education_degree,
education_field=education_field,
birthdate=birthdate,
iban=iban,
province=province,
city=city,
postal_code=postal_code,
address=address,
)
account.additional_properties = d
return account
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,106 +0,0 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
T = TypeVar("T", bound="Application")
@_attrs_define
class Application:
"""
Attributes:
uuid (UUID):
name (str):
avatar (Union[None, str]):
client_id (str):
user (UUID):
allowed_scope (str):
"""
uuid: UUID
name: str
avatar: Union[None, str]
client_id: str
user: UUID
allowed_scope: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid = str(self.uuid)
name = self.name
avatar: Union[None, str]
avatar = self.avatar
client_id = self.client_id
user = str(self.user)
allowed_scope = self.allowed_scope
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"uuid": uuid,
"name": name,
"avatar": avatar,
"client_id": client_id,
"user": user,
"allowed_scope": allowed_scope,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
uuid = UUID(d.pop("uuid"))
name = d.pop("name")
def _parse_avatar(data: object) -> Union[None, str]:
if data is None:
return data
return cast(Union[None, str], data)
avatar = _parse_avatar(d.pop("avatar"))
client_id = d.pop("client_id")
user = UUID(d.pop("user"))
allowed_scope = d.pop("allowed_scope")
application = cls(
uuid=uuid,
name=name,
avatar=avatar,
client_id=client_id,
user=user,
allowed_scope=allowed_scope,
)
application.additional_properties = d
return application
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,8 +0,0 @@
from enum import Enum
class BlankEnum(str, Enum):
VALUE_0 = ""
def __str__(self) -> str:
return str(self.value)

View file

@ -1,78 +0,0 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
T = TypeVar("T", bound="ChangePassword")
@_attrs_define
class ChangePassword:
"""
Attributes:
old_password (str):
new_password (str):
old_password_field (Union[Unset, str]): Default: 'password'.
"""
old_password: str
new_password: str
old_password_field: Union[Unset, str] = "password"
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
old_password = self.old_password
new_password = self.new_password
old_password_field = self.old_password_field
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"old_password": old_password,
"new_password": new_password,
}
)
if old_password_field is not UNSET:
field_dict["old_password_field"] = old_password_field
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
old_password = d.pop("old_password")
new_password = d.pop("new_password")
old_password_field = d.pop("old_password_field", UNSET)
change_password = cls(
old_password=old_password,
new_password=new_password,
old_password_field=old_password_field,
)
change_password.additional_properties = d
return change_password
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,14 +0,0 @@
from enum import Enum
class EducationDegreeEnum(str, Enum):
ASSOCIATE = "associate"
BACHELORS = "bachelors"
DOCTORATE = "doctorate"
HIGH = "high"
MASTERS = "masters"
MIDDLE = "middle"
PRIMARY = "primary"
def __str__(self) -> str:
return str(self.value)

View file

@ -1,9 +0,0 @@
from enum import Enum
class GenderEnum(str, Enum):
F = "f"
M = "m"
def __str__(self) -> str:
return str(self.value)

View file

@ -1,425 +0,0 @@
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
from ..models.blank_enum import BlankEnum
from ..models.education_degree_enum import EducationDegreeEnum
from ..models.gender_enum import GenderEnum
from ..types import UNSET, Unset
T = TypeVar("T", bound="PatchedAccount")
@_attrs_define
class PatchedAccount:
"""
Attributes:
uuid (Union[Unset, UUID]):
username (Union[None, Unset, str]): Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
email (Union[Unset, str]):
phone_number (Union[Unset, str]):
first_name (Union[Unset, str]):
last_name (Union[Unset, str]):
name (Union[Unset, str]):
avatar (Union[None, Unset, str]):
gender (Union[BlankEnum, GenderEnum, None, Unset]):
education_degree (Union[BlankEnum, EducationDegreeEnum, None, Unset]):
education_field (Union[None, Unset, str]):
birthdate (Union[None, Unset, datetime.date]):
iban (Union[None, Unset, str]):
iban_verified (Union[None, Unset, bool]):
province (Union[None, Unset, int]):
province_name (Union[Unset, str]):
city (Union[None, Unset, int]):
city_name (Union[Unset, str]):
postal_code (Union[None, Unset, str]):
address (Union[None, Unset, str]):
"""
uuid: Union[Unset, UUID] = UNSET
username: Union[None, Unset, str] = UNSET
email: Union[Unset, str] = UNSET
phone_number: Union[Unset, str] = UNSET
first_name: Union[Unset, str] = UNSET
last_name: Union[Unset, str] = UNSET
name: Union[Unset, str] = UNSET
avatar: Union[None, Unset, str] = UNSET
gender: Union[BlankEnum, GenderEnum, None, Unset] = UNSET
education_degree: Union[BlankEnum, EducationDegreeEnum, None, Unset] = UNSET
education_field: Union[None, Unset, str] = UNSET
birthdate: Union[None, Unset, datetime.date] = UNSET
iban: Union[None, Unset, str] = UNSET
iban_verified: Union[None, Unset, bool] = UNSET
province: Union[None, Unset, int] = UNSET
province_name: Union[Unset, str] = UNSET
city: Union[None, Unset, int] = UNSET
city_name: Union[Unset, str] = UNSET
postal_code: Union[None, Unset, str] = UNSET
address: Union[None, Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
uuid: Union[Unset, str] = UNSET
if not isinstance(self.uuid, Unset):
uuid = str(self.uuid)
username: Union[None, Unset, str]
if isinstance(self.username, Unset):
username = UNSET
else:
username = self.username
email = self.email
phone_number = self.phone_number
first_name = self.first_name
last_name = self.last_name
name = self.name
avatar: Union[None, Unset, str]
if isinstance(self.avatar, Unset):
avatar = UNSET
else:
avatar = self.avatar
gender: Union[None, Unset, str]
if isinstance(self.gender, Unset):
gender = UNSET
elif isinstance(self.gender, GenderEnum):
gender = self.gender.value
elif isinstance(self.gender, BlankEnum):
gender = self.gender.value
else:
gender = self.gender
education_degree: Union[None, Unset, str]
if isinstance(self.education_degree, Unset):
education_degree = UNSET
elif isinstance(self.education_degree, EducationDegreeEnum):
education_degree = self.education_degree.value
elif isinstance(self.education_degree, BlankEnum):
education_degree = self.education_degree.value
else:
education_degree = self.education_degree
education_field: Union[None, Unset, str]
if isinstance(self.education_field, Unset):
education_field = UNSET
else:
education_field = self.education_field
birthdate: Union[None, Unset, str]
if isinstance(self.birthdate, Unset):
birthdate = UNSET
elif isinstance(self.birthdate, datetime.date):
birthdate = self.birthdate.isoformat()
else:
birthdate = self.birthdate
iban: Union[None, Unset, str]
if isinstance(self.iban, Unset):
iban = UNSET
else:
iban = self.iban
iban_verified: Union[None, Unset, bool]
if isinstance(self.iban_verified, Unset):
iban_verified = UNSET
else:
iban_verified = self.iban_verified
province: Union[None, Unset, int]
if isinstance(self.province, Unset):
province = UNSET
else:
province = self.province
province_name = self.province_name
city: Union[None, Unset, int]
if isinstance(self.city, Unset):
city = UNSET
else:
city = self.city
city_name = self.city_name
postal_code: Union[None, Unset, str]
if isinstance(self.postal_code, Unset):
postal_code = UNSET
else:
postal_code = self.postal_code
address: Union[None, Unset, str]
if isinstance(self.address, Unset):
address = UNSET
else:
address = self.address
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if uuid is not UNSET:
field_dict["uuid"] = uuid
if username is not UNSET:
field_dict["username"] = username
if email is not UNSET:
field_dict["email"] = email
if phone_number is not UNSET:
field_dict["phone_number"] = phone_number
if first_name is not UNSET:
field_dict["first_name"] = first_name
if last_name is not UNSET:
field_dict["last_name"] = last_name
if name is not UNSET:
field_dict["name"] = name
if avatar is not UNSET:
field_dict["avatar"] = avatar
if gender is not UNSET:
field_dict["gender"] = gender
if education_degree is not UNSET:
field_dict["education_degree"] = education_degree
if education_field is not UNSET:
field_dict["education_field"] = education_field
if birthdate is not UNSET:
field_dict["birthdate"] = birthdate
if iban is not UNSET:
field_dict["iban"] = iban
if iban_verified is not UNSET:
field_dict["iban_verified"] = iban_verified
if province is not UNSET:
field_dict["province"] = province
if province_name is not UNSET:
field_dict["province_name"] = province_name
if city is not UNSET:
field_dict["city"] = city
if city_name is not UNSET:
field_dict["city_name"] = city_name
if postal_code is not UNSET:
field_dict["postal_code"] = postal_code
if address is not UNSET:
field_dict["address"] = address
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
_uuid = d.pop("uuid", UNSET)
uuid: Union[Unset, UUID]
if isinstance(_uuid, Unset):
uuid = UNSET
else:
uuid = UUID(_uuid)
def _parse_username(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
username = _parse_username(d.pop("username", UNSET))
email = d.pop("email", UNSET)
phone_number = d.pop("phone_number", UNSET)
first_name = d.pop("first_name", UNSET)
last_name = d.pop("last_name", UNSET)
name = d.pop("name", UNSET)
def _parse_avatar(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
avatar = _parse_avatar(d.pop("avatar", UNSET))
def _parse_gender(data: object) -> Union[BlankEnum, GenderEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
gender_type_0 = GenderEnum(data)
return gender_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
gender_type_1 = BlankEnum(data)
return gender_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, GenderEnum, None, Unset], data)
gender = _parse_gender(d.pop("gender", UNSET))
def _parse_education_degree(data: object) -> Union[BlankEnum, EducationDegreeEnum, None, Unset]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_0 = EducationDegreeEnum(data)
return education_degree_type_0
except: # noqa: E722
pass
try:
if not isinstance(data, str):
raise TypeError()
education_degree_type_1 = BlankEnum(data)
return education_degree_type_1
except: # noqa: E722
pass
return cast(Union[BlankEnum, EducationDegreeEnum, None, Unset], data)
education_degree = _parse_education_degree(d.pop("education_degree", UNSET))
def _parse_education_field(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
education_field = _parse_education_field(d.pop("education_field", UNSET))
def _parse_birthdate(data: object) -> Union[None, Unset, datetime.date]:
if data is None:
return data
if isinstance(data, Unset):
return data
try:
if not isinstance(data, str):
raise TypeError()
birthdate_type_0 = isoparse(data).date()
return birthdate_type_0
except: # noqa: E722
pass
return cast(Union[None, Unset, datetime.date], data)
birthdate = _parse_birthdate(d.pop("birthdate", UNSET))
def _parse_iban(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
iban = _parse_iban(d.pop("iban", UNSET))
def _parse_iban_verified(data: object) -> Union[None, Unset, bool]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, bool], data)
iban_verified = _parse_iban_verified(d.pop("iban_verified", UNSET))
def _parse_province(data: object) -> Union[None, Unset, int]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, int], data)
province = _parse_province(d.pop("province", UNSET))
province_name = d.pop("province_name", UNSET)
def _parse_city(data: object) -> Union[None, Unset, int]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, int], data)
city = _parse_city(d.pop("city", UNSET))
city_name = d.pop("city_name", UNSET)
def _parse_postal_code(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
postal_code = _parse_postal_code(d.pop("postal_code", UNSET))
def _parse_address(data: object) -> Union[None, Unset, str]:
if data is None:
return data
if isinstance(data, Unset):
return data
return cast(Union[None, Unset, str], data)
address = _parse_address(d.pop("address", UNSET))
patched_account = cls(
uuid=uuid,
username=username,
email=email,
phone_number=phone_number,
first_name=first_name,
last_name=last_name,
name=name,
avatar=avatar,
gender=gender,
education_degree=education_degree,
education_field=education_field,
birthdate=birthdate,
iban=iban,
iban_verified=iban_verified,
province=province,
province_name=province_name,
city=city,
city_name=city_name,
postal_code=postal_code,
address=address,
)
patched_account.additional_properties = d
return patched_account
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,77 +0,0 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from ..types import UNSET, Unset
T = TypeVar("T", bound="PatchedChangePassword")
@_attrs_define
class PatchedChangePassword:
"""
Attributes:
old_password (Union[Unset, str]):
old_password_field (Union[Unset, str]): Default: 'password'.
new_password (Union[Unset, str]):
"""
old_password: Union[Unset, str] = UNSET
old_password_field: Union[Unset, str] = "password"
new_password: Union[Unset, str] = UNSET
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
old_password = self.old_password
old_password_field = self.old_password_field
new_password = self.new_password
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
if old_password is not UNSET:
field_dict["old_password"] = old_password
if old_password_field is not UNSET:
field_dict["old_password_field"] = old_password_field
if new_password is not UNSET:
field_dict["new_password"] = new_password
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
old_password = d.pop("old_password", UNSET)
old_password_field = d.pop("old_password_field", UNSET)
new_password = d.pop("new_password", UNSET)
patched_change_password = cls(
old_password=old_password,
old_password_field=old_password_field,
new_password=new_password,
)
patched_change_password.additional_properties = d
return patched_change_password
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,113 +0,0 @@
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from uuid import UUID
from attrs import define as _attrs_define
from attrs import field as _attrs_field
T = TypeVar("T", bound="PublicUser")
@_attrs_define
class PublicUser:
"""
Attributes:
avatar (Union[None, str]):
name (str):
username (Union[None, str]): Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
first_name (str):
last_name (str):
uuid (UUID):
"""
avatar: Union[None, str]
name: str
username: Union[None, str]
first_name: str
last_name: str
uuid: UUID
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
avatar: Union[None, str]
avatar = self.avatar
name = self.name
username: Union[None, str]
username = self.username
first_name = self.first_name
last_name = self.last_name
uuid = str(self.uuid)
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"avatar": avatar,
"name": name,
"username": username,
"first_name": first_name,
"last_name": last_name,
"uuid": uuid,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
def _parse_avatar(data: object) -> Union[None, str]:
if data is None:
return data
return cast(Union[None, str], data)
avatar = _parse_avatar(d.pop("avatar"))
name = d.pop("name")
def _parse_username(data: object) -> Union[None, str]:
if data is None:
return data
return cast(Union[None, str], data)
username = _parse_username(d.pop("username"))
first_name = d.pop("first_name")
last_name = d.pop("last_name")
uuid = UUID(d.pop("uuid"))
public_user = cls(
avatar=avatar,
name=name,
username=username,
first_name=first_name,
last_name=last_name,
uuid=uuid,
)
public_user.additional_properties = d
return public_user
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,94 +0,0 @@
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
T = TypeVar("T", bound="RequestOTP")
@_attrs_define
class RequestOTP:
"""
Attributes:
phone_number (str):
otp_expire (Union[None, datetime.datetime]):
ttl (str):
"""
phone_number: str
otp_expire: Union[None, datetime.datetime]
ttl: str
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
phone_number = self.phone_number
otp_expire: Union[None, str]
if isinstance(self.otp_expire, datetime.datetime):
otp_expire = self.otp_expire.isoformat()
else:
otp_expire = self.otp_expire
ttl = self.ttl
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"phone_number": phone_number,
"otp_expire": otp_expire,
"ttl": ttl,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
phone_number = d.pop("phone_number")
def _parse_otp_expire(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
otp_expire_type_0 = isoparse(data)
return otp_expire_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
otp_expire = _parse_otp_expire(d.pop("otp_expire"))
ttl = d.pop("ttl")
request_otp = cls(
phone_number=phone_number,
otp_expire=otp_expire,
ttl=ttl,
)
request_otp.additional_properties = d
return request_otp
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,100 +0,0 @@
import datetime
from collections.abc import Mapping
from typing import Any, TypeVar, Union, cast
from attrs import define as _attrs_define
from attrs import field as _attrs_field
from dateutil.parser import isoparse
T = TypeVar("T", bound="RequestOTT")
@_attrs_define
class RequestOTT:
"""
Attributes:
application (str):
ott (Union[None, str]):
ott_expire (Union[None, datetime.datetime]):
"""
application: str
ott: Union[None, str]
ott_expire: Union[None, datetime.datetime]
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
def to_dict(self) -> dict[str, Any]:
application = self.application
ott: Union[None, str]
ott = self.ott
ott_expire: Union[None, str]
if isinstance(self.ott_expire, datetime.datetime):
ott_expire = self.ott_expire.isoformat()
else:
ott_expire = self.ott_expire
field_dict: dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update(
{
"application": application,
"ott": ott,
"ott_expire": ott_expire,
}
)
return field_dict
@classmethod
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
d = dict(src_dict)
application = d.pop("application")
def _parse_ott(data: object) -> Union[None, str]:
if data is None:
return data
return cast(Union[None, str], data)
ott = _parse_ott(d.pop("ott"))
def _parse_ott_expire(data: object) -> Union[None, datetime.datetime]:
if data is None:
return data
try:
if not isinstance(data, str):
raise TypeError()
ott_expire_type_0 = isoparse(data)
return ott_expire_type_0
except: # noqa: E722
pass
return cast(Union[None, datetime.datetime], data)
ott_expire = _parse_ott_expire(d.pop("ott_expire"))
request_ott = cls(
application=application,
ott=ott,
ott_expire=ott_expire,
)
request_ott.additional_properties = d
return request_ott
@property
def additional_keys(self) -> list[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Any:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Any) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View file

@ -1,8 +0,0 @@
"""A client library for accessing Notifications"""
from .client import AuthenticatedClient, Client
__all__ = (
"AuthenticatedClient",
"Client",
)

View file

@ -1 +0,0 @@
"""Contains methods for accessing the API"""

View file

@ -1 +0,0 @@
"""Contains endpoint functions for accessing the API"""

View file

@ -1,164 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.email import Email
from ...types import Response
def _get_kwargs(
user_uuid: str,
*,
body: Email,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": f"/email/application/{user_uuid}/application/",
}
_kwargs["json"] = body.to_dict()
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Email]:
if response.status_code == 201:
response_201 = Email.from_dict(response.json())
return response_201
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Email]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
body: Email,
) -> Response[Email]:
"""
Args:
user_uuid (str):
body (Email):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Email]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
user_uuid: str,
*,
client: AuthenticatedClient,
body: Email,
) -> Optional[Email]:
"""
Args:
user_uuid (str):
body (Email):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Email
"""
return sync_detailed(
user_uuid=user_uuid,
client=client,
body=body,
).parsed
async def asyncio_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
body: Email,
) -> Response[Email]:
"""
Args:
user_uuid (str):
body (Email):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Email]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
user_uuid: str,
*,
client: AuthenticatedClient,
body: Email,
) -> Optional[Email]:
"""
Args:
user_uuid (str):
body (Email):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Email
"""
return (
await asyncio_detailed(
user_uuid=user_uuid,
client=client,
body=body,
)
).parsed

View file

@ -1,183 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.paginated_email_list import PaginatedEmailList
from ...types import UNSET, Response, Unset
def _get_kwargs(
user_uuid: str,
*,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> dict[str, Any]:
params: dict[str, Any] = {}
params["limit"] = limit
params["offset"] = offset
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/email/application/{user_uuid}/application/",
"params": params,
}
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[PaginatedEmailList]:
if response.status_code == 200:
response_200 = PaginatedEmailList.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[PaginatedEmailList]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Response[PaginatedEmailList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PaginatedEmailList]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
limit=limit,
offset=offset,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Optional[PaginatedEmailList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PaginatedEmailList
"""
return sync_detailed(
user_uuid=user_uuid,
client=client,
limit=limit,
offset=offset,
).parsed
async def asyncio_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Response[PaginatedEmailList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PaginatedEmailList]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
limit=limit,
offset=offset,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Optional[PaginatedEmailList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PaginatedEmailList
"""
return (
await asyncio_detailed(
user_uuid=user_uuid,
client=client,
limit=limit,
offset=offset,
)
).parsed

View file

@ -1 +0,0 @@
"""Contains endpoint functions for accessing the API"""

View file

@ -1,164 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.push_message import PushMessage
from ...types import Response
def _get_kwargs(
user_uuid: str,
*,
body: PushMessage,
) -> dict[str, Any]:
headers: dict[str, Any] = {}
_kwargs: dict[str, Any] = {
"method": "post",
"url": f"/push/application/{user_uuid}/application/",
}
_kwargs["json"] = body.to_dict()
headers["Content-Type"] = "application/json"
_kwargs["headers"] = headers
return _kwargs
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[PushMessage]:
if response.status_code == 201:
response_201 = PushMessage.from_dict(response.json())
return response_201
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[PushMessage]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
body: PushMessage,
) -> Response[PushMessage]:
"""
Args:
user_uuid (str):
body (PushMessage):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PushMessage]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
body=body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
user_uuid: str,
*,
client: AuthenticatedClient,
body: PushMessage,
) -> Optional[PushMessage]:
"""
Args:
user_uuid (str):
body (PushMessage):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PushMessage
"""
return sync_detailed(
user_uuid=user_uuid,
client=client,
body=body,
).parsed
async def asyncio_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
body: PushMessage,
) -> Response[PushMessage]:
"""
Args:
user_uuid (str):
body (PushMessage):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PushMessage]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
body=body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
user_uuid: str,
*,
client: AuthenticatedClient,
body: PushMessage,
) -> Optional[PushMessage]:
"""
Args:
user_uuid (str):
body (PushMessage):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PushMessage
"""
return (
await asyncio_detailed(
user_uuid=user_uuid,
client=client,
body=body,
)
).parsed

View file

@ -1,183 +0,0 @@
from http import HTTPStatus
from typing import Any, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.paginated_push_message_list import PaginatedPushMessageList
from ...types import UNSET, Response, Unset
def _get_kwargs(
user_uuid: str,
*,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> dict[str, Any]:
params: dict[str, Any] = {}
params["limit"] = limit
params["offset"] = offset
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
_kwargs: dict[str, Any] = {
"method": "get",
"url": f"/push/application/{user_uuid}/application/",
"params": params,
}
return _kwargs
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[PaginatedPushMessageList]:
if response.status_code == 200:
response_200 = PaginatedPushMessageList.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[PaginatedPushMessageList]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Response[PaginatedPushMessageList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PaginatedPushMessageList]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
limit=limit,
offset=offset,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Optional[PaginatedPushMessageList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PaginatedPushMessageList
"""
return sync_detailed(
user_uuid=user_uuid,
client=client,
limit=limit,
offset=offset,
).parsed
async def asyncio_detailed(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Response[PaginatedPushMessageList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PaginatedPushMessageList]
"""
kwargs = _get_kwargs(
user_uuid=user_uuid,
limit=limit,
offset=offset,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
user_uuid: str,
*,
client: AuthenticatedClient,
limit: Union[Unset, int] = UNSET,
offset: Union[Unset, int] = UNSET,
) -> Optional[PaginatedPushMessageList]:
"""
Args:
user_uuid (str):
limit (Union[Unset, int]):
offset (Union[Unset, int]):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PaginatedPushMessageList
"""
return (
await asyncio_detailed(
user_uuid=user_uuid,
client=client,
limit=limit,
offset=offset,
)
).parsed

Some files were not shown because too many files have changed in this diff Show more