نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
@@ -0,0 +1,184 @@
"""Billing use cases.
Design rule, stated once and enforced here: **the internal usage ledger is the
source of truth.** A payment provider only moves money; it never decides what a
tenant owes. That is why ``summary`` computes totals from ``UsageLedgerRepository``
and not from any external invoice.
Credit is spent, not checked-then-spent: the debit is a single adjustment, so two
concurrent runs cannot both pass a stale affordability check.
"""
from __future__ import annotations
from datetime import datetime
from app.application.ports import PaymentProviderPort
from app.core.errors import NotFoundError, PermissionDeniedError, ValidationError
from app.domain.billing.models import (
CreditWallet,
Invoice,
Payment,
PaymentStatus,
Subscription,
)
from app.infrastructure.repositories.audit import UsageLedgerRepository
from app.infrastructure.repositories.billing import (
CreditWalletRepository,
InvoiceRepository,
PaymentRepository,
PlanRepository,
SubscriptionRepository,
)
#: A tenant may not buy an arbitrary amount of credit in one action.
MIN_TOPUP = 1.0
MAX_TOPUP = 10_000.0
class BillingService:
def __init__(
self,
*,
wallets: CreditWalletRepository,
invoices: InvoiceRepository,
payments: PaymentRepository,
plans: PlanRepository,
subscriptions: SubscriptionRepository,
ledger: UsageLedgerRepository,
payment_provider: PaymentProviderPort,
) -> None:
self.wallets = wallets
self.invoices = invoices
self.payments = payments
self.plans = plans
self.subscriptions = subscriptions
self.ledger = ledger
self.payment_provider = payment_provider
# -- reads ---------------------------------------------------------------
async def wallet_for(self, organization_id: str) -> CreditWallet:
return await self.wallets.ensure(organization_id)
async def summary(
self,
*,
organization_id: str,
since: datetime | None = None,
until: datetime | None = None,
) -> dict:
wallet = await self.wallets.ensure(organization_id)
breakdown = await self.ledger.breakdown(
organization_id=organization_id, since=since, until=until
)
return {
"balance": wallet.balance,
"currency": wallet.currency,
"low_balance": wallet.is_low,
"low_balance_threshold": wallet.low_balance_threshold,
"total_spend": breakdown.total_price,
"by_unit": breakdown.by_unit,
"by_employee": breakdown.by_employee,
"by_project": breakdown.by_project,
}
async def list_usage_events(
self, *, organization_id: str, limit: int = 100, offset: int = 0
) -> list:
return await self.ledger.list_events(
organization_id=organization_id, limit=limit, offset=offset
)
async def list_invoices(self, organization_id: str) -> list[Invoice]:
return await self.invoices.list_for_org(organization_id)
async def list_payments(self, organization_id: str) -> list[Payment]:
return await self.payments.list_for_org(organization_id)
async def current_subscription(self, organization_id: str) -> Subscription | None:
return await self.subscriptions.get_for_org(organization_id)
# -- writes --------------------------------------------------------------
async def add_credit(
self, *, organization_id: str, amount: float, allow_negative: bool = False
) -> CreditWallet:
"""Grant (or, for a platform owner, claw back) credit.
``allow_negative`` is reserved for the platform owner console; an org
admin may only ever add credit.
"""
if amount == 0:
raise ValidationError("مقدار اعتبار نمی‌تواند صفر باشد.")
if amount < 0 and not allow_negative:
raise PermissionDeniedError("کسر اعتبار فقط توسط مالک سامانه انجام می‌شود.")
if not allow_negative and amount > MAX_TOPUP:
raise ValidationError(f"حداکثر افزایش اعتبار در هر عملیات {MAX_TOPUP} است.")
wallet = await self.wallets.adjust(organization_id=organization_id, delta=amount)
if wallet.balance < 0:
# Refuse to leave a wallet in debt; undo and surface the conflict.
await self.wallets.adjust(organization_id=organization_id, delta=-amount)
raise ValidationError("اعتبار کافی برای این کسر وجود ندارد.")
return wallet
async def start_checkout(
self,
*,
organization_id: str,
amount: float,
success_url: str,
cancel_url: str,
) -> tuple[Payment, str]:
"""Create a pending payment plus the provider's hosted checkout URL."""
if amount < MIN_TOPUP:
raise ValidationError(f"حداقل مبلغ شارژ {MIN_TOPUP} است.")
if amount > MAX_TOPUP:
raise ValidationError(f"حداکثر مبلغ شارژ {MAX_TOPUP} است.")
wallet = await self.wallets.ensure(organization_id)
payment = await self.payments.create(
Payment(
organization_id=organization_id,
amount=amount,
status=PaymentStatus.PENDING,
provider="mock",
)
)
session = await self.payment_provider.create_checkout(
organization_id=organization_id,
amount=amount,
currency=wallet.currency,
success_url=success_url,
cancel_url=cancel_url,
)
updated = await self.payments.set_status(
payment.id, PaymentStatus.PENDING, external_id=session.external_id
)
return (updated or payment), session.url
async def settle_payment(
self, *, organization_id: str, payment_id: str, succeeded: bool
) -> Payment:
"""Mark a payment final and credit the wallet when it succeeded.
This is the only path that turns money into credit, so it is idempotent
with respect to status: a payment that is already final is returned
untouched rather than credited twice.
"""
payment = next(
(p for p in await self.payments.list_for_org(organization_id) if p.id == payment_id),
None,
)
if payment is None:
raise NotFoundError("پرداخت یافت نشد.")
if payment.status != PaymentStatus.PENDING:
return payment
if succeeded:
await self.wallets.adjust(organization_id=organization_id, delta=payment.amount)
final = await self.payments.set_status(payment_id, PaymentStatus.SUCCEEDED)
else:
final = await self.payments.set_status(payment_id, PaymentStatus.FAILED)
return final or payment