Skip to content

Commit

Permalink
Merge branch 'release/0.10.0' into preview
Browse files Browse the repository at this point in the history
  • Loading branch information
U-lis committed Jan 29, 2024
2 parents f19260f + 07c3abb commit df218d0
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
HEADLESS: ${{ vars.HEADLESS }}
ODIN_GQL_URL: ${{ vars.ODIN_GQL_URL }}
HEIMDALL_GQL_URL: ${{ vars.HEIMDALL_GQL_URL }}
KMS_KEY_ID: ${{ secrets.KMS_KEY_ID }}
GOOGLE_CREDENTIAL: ${{ secrets.GOOGLE_CREDENTIAL }}
GOOGLE_PACKAGE_NAME: ${{ vars.GOOGLE_PACKAGE_NAME }}
Expand Down Expand Up @@ -163,6 +165,8 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
HEADLESS: ${{ vars.HEADLESS }}
ODIN_GQL_URL: ${{ vars.ODIN_GQL_URL }}
HEIMDALL_GQL_URL: ${{ vars.HEIMDALL_GQL_URL }}
KMS_KEY_ID: ${{ secrets.KMS_KEY_ID }}
GOOGLE_CREDENTIAL: ${{ secrets.GOOGLE_CREDENTIAL }}
GOOGLE_PACKAGE_NAME: ${{ vars.GOOGLE_PACKAGE_NAME }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/synth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
HEADLESS: ${{ vars.HEADLESS }}
ODIN_GQL_URL: ${{ vars.ODIN_GQL_URL }}
HEIMDALL_GQL_URL: ${{ vars.HEIMDALL_GQL_URL }}
KMS_KEY_ID: ${{ secrets.KMS_KEY_ID }}
GOOGLE_CREDENTIAL: ${{ secrets.GOOGLE_CREDENTIAL }}
GOOGLE_PACKAGE_NAME: ${{ vars.GOOGLE_PACKAGE_NAME }}
Expand Down
2 changes: 2 additions & 0 deletions common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class Config:
account_id: str
region_name: str
cdn_host: str
odin_gql_url: str
heimdall_gql_url: str

# Multiplanetary
planet_url: str
Expand Down
59 changes: 59 additions & 0 deletions common/alembic/versions/90ff6ac09fe5_add_required_level_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Add required_level column
Revision ID: 90ff6ac09fe5
Revises: a2df38682595
Create Date: 2024-01-22 12:31:38.823682
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '90ff6ac09fe5'
down_revision = 'a2df38682595'
branch_labels = None
depends_on = None

old_enum = ("INIT", "VALIDATION_REQUEST", "VALID", "REFUNDED_BY_ADMIN", "INVALID", "REFUNDED_BY_BUYER",
"PURCHASE_LIMIT_EXCEED", "TIME_LIMIT", "UNKNOWN")
new_enum = sorted(old_enum + ("REQUIRED_LEVEL",))

old_status = sa.Enum(*old_enum, name="receiptstatus")
new_status = sa.Enum(*new_enum, name="receiptstatus")
tmp_status = sa.Enum(*new_enum, name="_receiptstatus")

receipt_table = sa.sql.table(
"receipt",
sa.Column("status", new_status, nullable=False)
)


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
tmp_status.create(op.get_bind(), checkfirst=False)
op.execute("ALTER TABLE receipt ALTER COLUMN status TYPE _receiptstatus USING status::text::_receiptstatus")
old_status.drop(op.get_bind(), checkfirst=False)
new_status.create(op.get_bind(), checkfirst=False)
op.execute("ALTER TABLE receipt ALTER COLUMN status TYPE receiptstatus USING status::text::receiptstatus")
tmp_status.drop(op.get_bind(), checkfirst=False)

op.add_column('product', sa.Column('required_level', sa.Integer(), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('product', 'required_level')

op.execute(receipt_table.update()
.where(receipt_table.c.status.in_(['REQUIRED_LEVEL']))
.values(status="INVALID")
)
tmp_status.create(op.get_bind(), checkfirst=False)
op.execute("ALTER TABLE receipt ALTER COLUMN status TYPE _receiptstatus USING status::text::_receiptstatus")
new_status.drop(op.get_bind(), checkfirst=False)
old_status.create(op.get_bind(), checkfirst=False)
op.execute("ALTER TABLE receipt ALTER COLUMN status TYPE receiptstatus USING status::text::receiptstatus")
tmp_status.drop(op.get_bind(), checkfirst=False)
# ### end Alembic commands ###
30 changes: 30 additions & 0 deletions common/alembic/versions/a2df38682595_add_is_free_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Add is_free column
Revision ID: a2df38682595
Revises: 2822c456abc3
Create Date: 2024-01-18 13:55:23.731546
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'a2df38682595'
down_revision = '2822c456abc3'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('product', sa.Column('is_free', sa.Boolean(), nullable=True, default=False))
op.execute("UPDATE product SET is_free=FALSE")
op.alter_column("product", "is_free", nullable=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('product', 'is_free')
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class ReceiptStatus(IntEnum):
Purchase timestamp is not between target product's open/close timestamp.
This purchase is executed in appstore, so admin should refund this purchase in manual.
- **95: `REQUIRED_LEVEL`**
Does not met required level to get this product.
- **99: `UNKNOWN`**
An unhandled error case. This is reserve to catch all other errors.
Expand All @@ -145,6 +149,7 @@ class ReceiptStatus(IntEnum):
REFUNDED_BY_BUYER = 92
PURCHASE_LIMIT_EXCEED = 93
TIME_LIMIT = 94
REQUIRED_LEVEL = 95
UNKNOWN = 99


Expand Down
9 changes: 5 additions & 4 deletions common/lib9c/models/fungible_asset_value.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from decimal import Decimal
from typing import Dict, List, Optional, Any

import bencodex
Expand All @@ -8,7 +9,7 @@


class FungibleAssetValue:
def __init__(self, currency: Currency, amount: float):
def __init__(self, currency: Currency, amount: Decimal):
self.currency = currency
self.amount = amount

Expand All @@ -19,16 +20,16 @@ def __eq__(self, other: FungibleAssetValue):
def from_raw_data(
cls,
ticker: str, decimal_places: int, minters: Optional[List[str]] = None, total_supply_trackable: bool = False,
amount: float = 0
amount: Decimal = Decimal("0")
):
return cls(
Currency(ticker, decimal_places, minters, total_supply_trackable),
amount=amount
)

@property
def plain_value(self) -> List[Dict[str, Any] | float]:
return [self.currency.plain_value, self.amount * max(1, 10 ** self.currency.decimal_places)]
def plain_value(self) -> List[Dict[str, Any] | int]:
return [self.currency.plain_value, int(self.amount * max(1, 10 ** self.currency.decimal_places))]

@property
def serialized_plain_value(self) -> bytes:
Expand Down
2 changes: 2 additions & 0 deletions common/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class Product(AutoIdMixin, TimeStampMixin, Base):
google_sku = Column(Text, doc="SKU ID of google play store")
apple_sku = Column(Text, doc="SKU ID of apple appstore")
# product_type = Column(ENUM(ProductType), default=ProductType.SINGLE, nullable=False)
is_free = Column(Boolean, nullable=False, default=False, doc="Flag to set this product as free")
required_level = Column(Integer, nullable=True, default=None, doc="Required avatar level to purchase this product")
daily_limit = Column(Integer, nullable=True, doc="Purchase limit in 24 hours")
weekly_limit = Column(Integer, nullable=True, doc="Purchase limit in 7 days (24 * 7 hours)")
account_limit = Column(Integer, nullable=True, doc="Purchase limit for each account (in lifetime)")
Expand Down
121 changes: 117 additions & 4 deletions iap/api/purchase.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import os
from datetime import datetime
from typing import List, Dict, Optional, Annotated
from uuid import UUID
from uuid import UUID, uuid4

import boto3
import requests
from fastapi import APIRouter, Depends, Query
from googleapiclient.errors import HttpError
from sqlalchemy import select
from sqlalchemy import select, or_
from sqlalchemy.orm import joinedload
from starlette.responses import JSONResponse

Expand All @@ -23,10 +23,10 @@
from iap.dependencies import session
from iap.exceptions import ReceiptNotFoundException
from iap.main import logger
from iap.schemas.receipt import ReceiptSchema, ReceiptDetailSchema
from iap.schemas.receipt import ReceiptSchema, ReceiptDetailSchema, FreeReceiptSchema
from iap.utils import create_season_pass_jwt, get_purchase_count
from iap.validator.common import get_order_data
from iap.validator.apple import validate_apple
from iap.validator.common import get_order_data
from iap.validator.google import validate_google

router = APIRouter(
Expand Down Expand Up @@ -307,6 +307,119 @@ def request_product(receipt_data: ReceiptSchema, sess=Depends(session)):
return receipt


@router.post("/free", response_model=ReceiptDetailSchema)
def free_product(receipt_data: FreeReceiptSchema, sess=Depends(session)):
"""
# Purchase Free Product
---
**Purchase free product and unload product from IAP garage to buyer.**
### Request Body
- `store` :: int : Store type in IntEnum. Please see `StoreType` Enum.
- `agentAddress` :: str : 9c agent address of buyer.
- `avatarAddress` :: str : 9c avatar address to get items.
- `sku` :: str : Purchased product SKU
"""
if not receipt_data.planetId:
raise ReceiptNotFoundException("", "")

product = sess.scalar(
select(Product)
.options(joinedload(Product.fav_list)).options(joinedload(Product.fungible_item_list))
.where(
Product.active.is_(True),
or_(Product.google_sku == receipt_data.sku, Product.apple_sku == receipt_data.sku)
)
)
order_id = f"FREE-{uuid4()}"
receipt = Receipt(
store=receipt_data.store,
data={"SKU": receipt_data.sku, "OrderId": order_id},
agent_addr=receipt_data.agentAddress.lower(),
avatar_addr=receipt_data.avatarAddress.lower(),
order_id=order_id,
purchased_at=datetime.utcnow(),
product_id=product.id if product is not None else None,
planet_id=receipt_data.planetId.value,
)
sess.add(receipt)
sess.commit()
sess.refresh(receipt)

# Validation
if not product:
receipt.status = ReceiptStatus.INVALID
receipt.msg = f"Product {receipt_data.product_id} not exists or inactive"
raise_error(sess, receipt, ValueError(f"Product {receipt_data.product_id} not found or inactive"))

if not product.is_free:
receipt.status = ReceiptStatus.INVALID
receipt.msg = "This product it not for free"
raise_error(sess, receipt, ValueError(f"Requested product {product.id}::{product.name} is not for free"))

if ((product.open_timestamp and product.open_timestamp > datetime.now()) or
(product.close_timestamp and product.close_timestamp < datetime.now())):
receipt.status = ReceiptStatus.TIME_LIMIT
raise_error(sess, receipt, ValueError(f"Not in product opening time"))

# Purchase Limit
if (product.daily_limit and
get_purchase_count(sess, product.id, planet_id=PlanetID(receipt.planet_id),
agent_addr=receipt.agent_addr.lower(), daily_limit=True) > product.daily_limit):
receipt.status = ReceiptStatus.PURCHASE_LIMIT_EXCEED
raise_error(sess, receipt, ValueError("Daily purchase limit exceeded."))
elif (product.weekly_limit and
get_purchase_count(sess, product.id, planet_id=PlanetID(receipt.planet_id),
agent_addr=receipt.agent_addr.lower(), weekly_limit=True) > product.weekly_limit):
receipt.status = ReceiptStatus.PURCHASE_LIMIT_EXCEED
raise_error(sess, receipt, ValueError("Weekly purchase limit exceeded."))
elif (product.account_limit and
get_purchase_count(sess, product.id, planet_id=PlanetID(receipt.planet_id),
agent_addr=receipt.agent_addr.lower()) > product.account_limit):
receipt.status = ReceiptStatus.PURCHASE_LIMIT_EXCEED
raise_error(sess, receipt, ValueError("Account purchase limit exceeded."))

# Required level
if product.required_level:
gql_url = None
if receipt_data.planetId in (PlanetID.ODIN, PlanetID.ODIN_INTERNAL):
gql_url = os.environ.get("ODIN_GQL_URL")
elif receipt_data.planetId in (PlanetID.HEIMDALL, PlanetID.HEIMDALL_INTERNAL):
gql_url = os.environ.get("HEIMDALL_GQL_URL")

query = f"""{{ stateQuery {{ avatar (avatarAddress: "{receipt_data.avatarAddress}") {{ level}} }} }}"""
try:
resp = requests.post(gql_url, json={"query": query}, timeout=1)
avatar_level = resp.json()["data"]["stateQuery"]["avatar"]["level"]
except:
# Whether request is failed or no fitted data found
avatar_level = 0

if avatar_level < product.required_level:
receipt.status = ReceiptStatus.REQUIRED_LEVEL
raise_error(sess, receipt,
ValueError(f"Avatar level {avatar_level} does not met required level {product.required_level}"))

receipt.status = ReceiptStatus.VALID
sess.add(receipt)
sess.commit()
sess.refresh(receipt)

msg = {
"agent_addr": receipt_data.agentAddress.lower(),
"avatar_addr": receipt_data.avatarAddress.lower(),
"product_id": product.id,
"uuid": str(receipt.uuid),
"planet_id": receipt_data.planetId.decode('utf-8'),
}

resp = sqs.send_message(QueueUrl=SQS_URL, MessageBody=json.dumps(msg))
logger.debug(f"message [{resp['MessageId']}] sent to SQS.")

return receipt


@router.get("/status", response_model=Dict[UUID, Optional[ReceiptDetailSchema]])
def purchase_status(uuid: Annotated[List[UUID], Query()] = ..., sess=Depends(session)):
"""
Expand Down
2 changes: 2 additions & 0 deletions iap/iap_cdk_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
"APPLE_KEY_ID": config.apple_key_id,
"APPLE_ISSUER_ID": config.apple_issuer_id,
"HEADLESS": config.headless,
"ODIN_GQL_URL": config.odin_gql_url,
"HEIMDALL_GQL_URL": config.heimdall_gql_url,
"CDN_HOST": config.cdn_host,
"PLANET_URL": config.planet_url,
"BRIDGE_DATA": config.bridge_data,
Expand Down
8 changes: 5 additions & 3 deletions iap/schemas/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

from pydantic import BaseModel as BaseSchema, model_validator

from common.enums import Currency, ProductRarity, ProductAssetUISize
from common.enums import ProductRarity, ProductAssetUISize


class SimpleProductSchema(BaseSchema):
name: str
order: int
google_sku: str
apple_sku: str
google_sku: str = ""
apple_sku: str = ""
is_free: bool
# product_type: ProductType
daily_limit: Optional[int] = None
weekly_limit: Optional[int] = None
account_limit: Optional[int] = None
active: bool
buyable: bool = True
required_level: Optional[int] = None

class Config:
from_attributes = True
Expand Down
Loading

0 comments on commit df218d0

Please sign in to comment.