allnet: save billing traces
parent
ada9377c06
commit
ce475e801b
125
core/allnet.py
125
core/allnet.py
|
@ -586,31 +586,12 @@ class BillingServlet:
|
|||
rsa = RSA.import_key(open(self.config.billing.signing_key, "rb").read())
|
||||
signer = PKCS1_v1_5.new(rsa)
|
||||
digest = SHA.new()
|
||||
traces: List[TraceData] = []
|
||||
try:
|
||||
req = BillingInfo(req_dict[0])
|
||||
except KeyError as e:
|
||||
self.logger.error(f"Billing request failed to parse: {e}")
|
||||
return PlainTextResponse("result=5&linelimit=&message=field is missing or formatting is incorrect\r\n")
|
||||
|
||||
for x in range(1, len(req_dict)):
|
||||
if not req_dict[x]:
|
||||
continue
|
||||
|
||||
try:
|
||||
tmp = TraceData(req_dict[x])
|
||||
if tmp.trace_type == TraceDataType.CHARGE:
|
||||
tmp = TraceDataCharge(req_dict[x])
|
||||
elif tmp.trace_type == TraceDataType.EVENT:
|
||||
tmp = TraceDataEvent(req_dict[x])
|
||||
elif tmp.trace_type == TraceDataType.CREDIT:
|
||||
tmp = TraceDataCredit(req_dict[x])
|
||||
|
||||
traces.append(tmp)
|
||||
|
||||
except KeyError as e:
|
||||
self.logger.warning(f"Tracelog failed to parse: {e}")
|
||||
|
||||
kc_serial_bytes = req.keychipid.encode()
|
||||
|
||||
|
||||
|
@ -618,7 +599,7 @@ class BillingServlet:
|
|||
if machine is None and not self.config.server.allow_unregistered_serials:
|
||||
msg = f"Unrecognised serial {req.keychipid} attempted billing checkin from {request_ip} for {req.gameid} v{req.gamever}."
|
||||
await self.data.base.log_event(
|
||||
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=req.gamever
|
||||
"allnet", "BILLING_CHECKIN_NG_SERIAL", logging.WARN, msg, ip=request_ip, game=req.gameid, version=str(req.gamever)
|
||||
)
|
||||
self.logger.warning(msg)
|
||||
|
||||
|
@ -629,18 +610,79 @@ class BillingServlet:
|
|||
"billing_type": req.billingtype.name,
|
||||
"nearfull": req.nearfull,
|
||||
"playlimit": req.playlimit,
|
||||
"messages": []
|
||||
}
|
||||
|
||||
if machine is not None:
|
||||
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, "", log_details, None, machine['arcade'], machine['id'], request_ip, req.gameid, req.gamever)
|
||||
for x in range(1, len(req_dict)):
|
||||
if not req_dict[x]:
|
||||
continue
|
||||
|
||||
try:
|
||||
tmp = TraceData(req_dict[x])
|
||||
if tmp.trace_type == TraceDataType.CHARGE:
|
||||
tmp = TraceDataCharge(req_dict[x])
|
||||
if self.config.allnet.save_billing:
|
||||
await self.data.arcade.billing_add_charge(
|
||||
machine['id'],
|
||||
tmp.game_id,
|
||||
float(tmp.game_version),
|
||||
tmp.play_count,
|
||||
tmp.play_limit,
|
||||
tmp.product_code,
|
||||
tmp.product_count,
|
||||
tmp.func_type,
|
||||
tmp.player_number
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
f"Charge Trace from {req.keychipid}: {tmp.game_id} v{tmp.game_version} - player {tmp.player_number} got {tmp.product_count} of {tmp.product_code} func {tmp.func_type}"
|
||||
)
|
||||
|
||||
elif tmp.trace_type == TraceDataType.EVENT:
|
||||
tmp = TraceDataEvent(req_dict[x])
|
||||
log_details['messages'].append(tmp.message)
|
||||
self.logger.info(f"Event Trace from {req.keychipid}: {tmp.message}")
|
||||
|
||||
elif tmp.trace_type == TraceDataType.CREDIT:
|
||||
tmp = TraceDataCredit(req_dict[x])
|
||||
if self.config.allnet.save_billing:
|
||||
await self.data.arcade.billing_set_credit(
|
||||
machine['id'],
|
||||
tmp.chute_type.value,
|
||||
tmp.service_type.value,
|
||||
tmp.operation_type.value,
|
||||
tmp.coin_rate0,
|
||||
tmp.coin_rate1,
|
||||
tmp.bonus_addition,
|
||||
tmp.credit_rate,
|
||||
tmp.credit0,
|
||||
tmp.credit1,
|
||||
tmp.credit2,
|
||||
tmp.credit3,
|
||||
tmp.credit4,
|
||||
tmp.credit5,
|
||||
tmp.credit6,
|
||||
tmp.credit7
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
f"Credit Trace from {req.keychipid}: {tmp.operation_type} mode, {tmp.credit_rate} coins per credit, Consumed {tmp.credit0} | {tmp.credit1} | {tmp.credit2} | {tmp.credit3} | {tmp.credit4} | {tmp.credit5} | {tmp.credit6} | {tmp.credit7} | "
|
||||
)
|
||||
|
||||
except KeyError as e:
|
||||
self.logger.warning(f"Tracelog failed to parse: {e}")
|
||||
|
||||
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK", logging.INFO, "", log_details, None, machine['arcade'], machine['id'], request_ip, req.gameid, str(req.gamever))
|
||||
|
||||
self.logger.info(
|
||||
f"Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
|
||||
f"{req.playcnt} billing_type {req.billingtype.name} nearfull {req.nearfull} playlimit {req.playlimit}"
|
||||
)
|
||||
|
||||
else:
|
||||
log_details['serial'] = req.keychipid
|
||||
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK_UNREG", logging.INFO, "", log_details, None, None, None, request_ip, req.gameid, req.gamever)
|
||||
await self.data.base.log_event("billing", "BILLING_CHECKIN_OK_UNREG", logging.INFO, "", log_details, None, None, None, request_ip, req.gameid, str(req.gamever))
|
||||
|
||||
self.logger.info(
|
||||
f"Unregistered Billing checkin from {request_ip}: game {req.gameid} ver {req.gamever} keychip {req.keychipid} playcount "
|
||||
|
@ -768,14 +810,27 @@ class BillingType(Enum):
|
|||
A = 1
|
||||
B = 0
|
||||
|
||||
class TraceDataCreditChuteType(Enum):
|
||||
COMMON = 0
|
||||
INDIVIDUAL = 1
|
||||
|
||||
class TraceDataCreditOperationType(Enum):
|
||||
COIN = 0
|
||||
FREEPLAY = 1
|
||||
|
||||
class float5:
|
||||
def __init__(self, n: str = "0") -> None:
|
||||
def __init__(self, n: str = "0"):
|
||||
nf = float(n)
|
||||
if nf > 999.9 or nf < 0:
|
||||
raise ValueError('float5 must be between 0.000 and 999.9 inclusive')
|
||||
|
||||
return nf
|
||||
self.val = nf
|
||||
|
||||
def __float__(self) -> float:
|
||||
return self.val
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"%.{2 - int(math.log10(self.val))+1}f" % self.val
|
||||
|
||||
@classmethod
|
||||
def to_str(cls, f: float):
|
||||
return f"%.{2 - int(math.log10(f))+1}f" % f
|
||||
|
@ -786,13 +841,13 @@ class BillingInfo:
|
|||
self.keychipid = str(data.get("keychipid", None))
|
||||
self.functype = int(data.get("functype", None))
|
||||
self.gameid = str(data.get("gameid", None))
|
||||
self.gamever = float(data.get("gamever", None))
|
||||
self.gamever = float5(data.get("gamever", None))
|
||||
self.boardid = str(data.get("boardid", None))
|
||||
self.tenpoip = str(data.get("tenpoip", None))
|
||||
self.libalibver = float(data.get("libalibver", None))
|
||||
self.libalibver = float5(data.get("libalibver", None))
|
||||
self.datamax = int(data.get("datamax", None))
|
||||
self.billingtype = BillingType(int(data.get("billingtype", None)))
|
||||
self.protocolver = float(data.get("protocolver", None))
|
||||
self.protocolver = float5(data.get("protocolver", None))
|
||||
self.operatingfix = bool(data.get("operatingfix", None))
|
||||
self.traceleft = int(data.get("traceleft", None))
|
||||
self.requestno = int(data.get("requestno", None))
|
||||
|
@ -825,7 +880,7 @@ class TraceData:
|
|||
self.date = datetime.strptime(data.get("dt", None), BILLING_DT_FORMAT)
|
||||
|
||||
self.keychip = str(data.get("kn", None))
|
||||
self.lib_ver = float(data.get("alib", 0))
|
||||
self.lib_ver = float5(data.get("alib", 0))
|
||||
except Exception as e:
|
||||
raise KeyError(e)
|
||||
|
||||
|
@ -834,7 +889,7 @@ class TraceDataCharge(TraceData):
|
|||
super().__init__(data)
|
||||
try:
|
||||
self.game_id = str(data.get("gi", None)) # these seem optional...?
|
||||
self.game_version = float(data.get("gv", 0))
|
||||
self.game_version = float5(data.get("gv", 0))
|
||||
self.board_serial = str(data.get("bn", None))
|
||||
self.shop_ip = str(data.get("ti", None))
|
||||
self.play_count = int(data.get("pc", None))
|
||||
|
@ -858,9 +913,9 @@ class TraceDataCredit(TraceData):
|
|||
def __init__(self, data: Dict) -> None:
|
||||
super().__init__(data)
|
||||
try:
|
||||
self.chute_type = int(data.get("cct", None))
|
||||
self.service_type = int(data.get("cst", None))
|
||||
self.operation_type = int(data.get("cop", None))
|
||||
self.chute_type = TraceDataCreditChuteType(int(data.get("cct", None)))
|
||||
self.service_type = TraceDataCreditChuteType(int(data.get("cst", None)))
|
||||
self.operation_type = TraceDataCreditOperationType(int(data.get("cop", None)))
|
||||
self.coin_rate0 = int(data.get("cr0", None))
|
||||
self.coin_rate1 = int(data.get("cr1", None))
|
||||
self.bonus_addition = int(data.get("cba", None))
|
||||
|
@ -884,7 +939,7 @@ class BillingResponse:
|
|||
nearfull: str = "",
|
||||
nearfull_sig: str = "",
|
||||
request_num: int = 1,
|
||||
protocol_ver: float = 1.000,
|
||||
protocol_ver: float5 = float5("1.000"),
|
||||
playhistory: str = "000000/0:000000/0:000000/0",
|
||||
) -> None:
|
||||
self.result = 0
|
||||
|
@ -898,7 +953,7 @@ class BillingResponse:
|
|||
self.nearfull = nearfull
|
||||
self.nearfullsig = nearfull_sig
|
||||
self.linelimit = 100
|
||||
self.protocolver = float5.to_str(protocol_ver)
|
||||
self.protocolver = str(protocol_ver)
|
||||
# playhistory -> YYYYMM/C:...
|
||||
# YYYY -> 4 digit year, MM -> 2 digit month, C -> Playcount during that period
|
||||
|
||||
|
|
|
@ -362,7 +362,7 @@ class AllnetConfig:
|
|||
)
|
||||
|
||||
@property
|
||||
def allow_online_updates(self) -> int:
|
||||
def allow_online_updates(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "allow_online_updates", default=False
|
||||
)
|
||||
|
@ -373,6 +373,12 @@ class AllnetConfig:
|
|||
self.__config, "core", "allnet", "update_cfg_folder", default=""
|
||||
)
|
||||
|
||||
@property
|
||||
def save_billing(self) -> bool:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "allnet", "save_billing", default=False
|
||||
)
|
||||
|
||||
class BillingConfig:
|
||||
def __init__(self, parent_config: "CoreConfig") -> None:
|
||||
self.__config = parent_config
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
"""add_billing_tables
|
||||
|
||||
Revision ID: 27e3434740df
|
||||
Revises: ae364c078429
|
||||
Create Date: 2025-04-17 18:32:06.008601
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '27e3434740df'
|
||||
down_revision = 'ae364c078429'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('machine_billing_charge',
|
||||
sa.Column('id', sa.BIGINT(), nullable=False),
|
||||
sa.Column('machine', sa.Integer(), nullable=False),
|
||||
sa.Column('game_id', sa.CHAR(length=5), nullable=False),
|
||||
sa.Column('game_ver', sa.FLOAT(), nullable=False),
|
||||
sa.Column('play_count', sa.INTEGER(), nullable=False),
|
||||
sa.Column('play_limit', sa.INTEGER(), nullable=False),
|
||||
sa.Column('product_code', sa.INTEGER(), nullable=False),
|
||||
sa.Column('product_count', sa.INTEGER(), nullable=False),
|
||||
sa.Column('func_type', sa.INTEGER(), nullable=False),
|
||||
sa.Column('player_number', sa.INTEGER(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['machine'], ['machine.id'], onupdate='cascade', ondelete='cascade'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_charset='utf8mb4'
|
||||
)
|
||||
op.create_table('machine_billing_credit',
|
||||
sa.Column('id', sa.BIGINT(), nullable=False),
|
||||
sa.Column('machine', sa.Integer(), nullable=False),
|
||||
sa.Column('chute_type', sa.INTEGER(), nullable=False),
|
||||
sa.Column('service_type', sa.INTEGER(), nullable=False),
|
||||
sa.Column('operation_type', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_rate0', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_rate1', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_bonus', sa.INTEGER(), nullable=False),
|
||||
sa.Column('credit_rate', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot0', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot1', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot2', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot3', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot4', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot5', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot6', sa.INTEGER(), nullable=False),
|
||||
sa.Column('coin_count_slot7', sa.INTEGER(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['machine'], ['machine.id'], onupdate='cascade', ondelete='cascade'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('machine'),
|
||||
mysql_charset='utf8mb4'
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('machine_billing_credit')
|
||||
op.drop_table('machine_billing_charge')
|
||||
# ### end Alembic commands ###
|
|
@ -6,7 +6,7 @@ from sqlalchemy.dialects.mysql import insert
|
|||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.types import JSON, Boolean, Integer, String
|
||||
from sqlalchemy.types import JSON, Boolean, Integer, String, BIGINT, INTEGER, CHAR, FLOAT
|
||||
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
|
||||
|
@ -67,6 +67,56 @@ arcade_owner: Table = Table(
|
|||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
billing_charge: Table = Table(
|
||||
"machine_billing_charge",
|
||||
metadata,
|
||||
Column("id", BIGINT, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"machine",
|
||||
Integer,
|
||||
ForeignKey("machine.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False,
|
||||
),
|
||||
Column("game_id", CHAR(5), nullable=False),
|
||||
Column("game_ver", FLOAT, nullable=False),
|
||||
Column("play_count", INTEGER, nullable=False),
|
||||
Column("play_limit", INTEGER, nullable=False),
|
||||
Column("product_code", INTEGER, nullable=False),
|
||||
Column("product_count", INTEGER, nullable=False),
|
||||
Column("func_type", INTEGER, nullable=False),
|
||||
Column("player_number", INTEGER, nullable=False),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
# These settings are only really of interest
|
||||
# for real cabinets operating as pay-to-play
|
||||
billing_credit: Table = Table(
|
||||
"machine_billing_credit",
|
||||
metadata,
|
||||
Column("id", BIGINT, primary_key=True, nullable=False),
|
||||
Column(
|
||||
"machine",
|
||||
Integer,
|
||||
ForeignKey("machine.id", ondelete="cascade", onupdate="cascade"),
|
||||
nullable=False, unique=True
|
||||
),
|
||||
Column("chute_type", INTEGER, nullable=False),
|
||||
Column("service_type", INTEGER, nullable=False),
|
||||
Column("operation_type", INTEGER, nullable=False),
|
||||
Column("coin_rate0", INTEGER, nullable=False),
|
||||
Column("coin_rate1", INTEGER, nullable=False),
|
||||
Column("coin_bonus", INTEGER, nullable=False),
|
||||
Column("credit_rate", INTEGER, nullable=False),
|
||||
Column("coin_count_slot0", INTEGER, nullable=False),
|
||||
Column("coin_count_slot1", INTEGER, nullable=False),
|
||||
Column("coin_count_slot2", INTEGER, nullable=False),
|
||||
Column("coin_count_slot3", INTEGER, nullable=False),
|
||||
Column("coin_count_slot4", INTEGER, nullable=False),
|
||||
Column("coin_count_slot5", INTEGER, nullable=False),
|
||||
Column("coin_count_slot6", INTEGER, nullable=False),
|
||||
Column("coin_count_slot7", INTEGER, nullable=False),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
class ArcadeData(BaseData):
|
||||
async def get_machine(self, serial: Optional[str] = None, id: Optional[int] = None) -> Optional[Row]:
|
||||
|
@ -345,6 +395,71 @@ class ArcadeData(BaseData):
|
|||
return result.fetchone()['count_1']
|
||||
self.logger.error("Failed to count machine serials that start with A69A!")
|
||||
|
||||
async def billing_add_charge(self, machine_id: int, game_id: str, game_ver: float, playcount: int, playlimit, product_code: int, product_count: int, func_type: int, player_num: int) -> Optional[int]:
|
||||
result = await self.execute(billing_charge.insert().values(
|
||||
machine=machine_id,
|
||||
game_id=game_id,
|
||||
game_ver=game_ver,
|
||||
play_count=playcount,
|
||||
play_limit=playlimit,
|
||||
product_code=product_code,
|
||||
product_count=product_count,
|
||||
func_type=func_type,
|
||||
player_num=player_num
|
||||
))
|
||||
|
||||
if result is None:
|
||||
self.logger.error(f"Failed to add billing charge for machine {machine_id}!")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
async def billing_set_credit(self, machine_id: int, chute_type: int, service_type: int, op_mode: int, coin_rate0: int, coin_rate1: int,
|
||||
bonus_adder: int, coin_to_credit_rate: int, coin_count_slot0: int, coin_count_slot1: int, coin_count_slot2: int, coin_count_slot3: int,
|
||||
coin_count_slot4: int, coin_count_slot5: int, coin_count_slot6: int, coin_count_slot7: int) -> Optional[int]:
|
||||
|
||||
sql = insert(billing_credit).values(
|
||||
machine=machine_id,
|
||||
chute_type=chute_type,
|
||||
service_type=service_type,
|
||||
operation_type=op_mode,
|
||||
coin_rate0=coin_rate0,
|
||||
coin_rate1=coin_rate1,
|
||||
coin_bonus=bonus_adder,
|
||||
credit_rate=coin_to_credit_rate,
|
||||
coin_count_slot0=coin_count_slot0,
|
||||
coin_count_slot1=coin_count_slot1,
|
||||
coin_count_slot2=coin_count_slot2,
|
||||
coin_count_slot3=coin_count_slot3,
|
||||
coin_count_slot4=coin_count_slot4,
|
||||
coin_count_slot5=coin_count_slot5,
|
||||
coin_count_slot6=coin_count_slot6,
|
||||
coin_count_slot7=coin_count_slot7,
|
||||
)
|
||||
|
||||
conflict = sql.on_duplicate_key_update(
|
||||
chute_type=chute_type,
|
||||
service_type=service_type,
|
||||
operation_type=op_mode,
|
||||
coin_rate0=coin_rate0,
|
||||
coin_rate1=coin_rate1,
|
||||
coin_bonus=bonus_adder,
|
||||
credit_rate=coin_to_credit_rate,
|
||||
coin_count_slot0=coin_count_slot0,
|
||||
coin_count_slot1=coin_count_slot1,
|
||||
coin_count_slot2=coin_count_slot2,
|
||||
coin_count_slot3=coin_count_slot3,
|
||||
coin_count_slot4=coin_count_slot4,
|
||||
coin_count_slot5=coin_count_slot5,
|
||||
coin_count_slot6=coin_count_slot6,
|
||||
coin_count_slot7=coin_count_slot7,
|
||||
)
|
||||
|
||||
result = await self.execute(conflict)
|
||||
if result is None:
|
||||
self.logger.error(f"Failed to set billing credit settings for machine {machine_id}!")
|
||||
return None
|
||||
return result.lastrowid
|
||||
|
||||
def format_serial(
|
||||
self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False
|
||||
) -> str:
|
||||
|
@ -371,7 +486,6 @@ class ArcadeData(BaseData):
|
|||
month = ((month - 1) + 9) % 12 # Offset so April=0
|
||||
return f"{year:02}{month // 6:01}{month % 6 + 1:01}"
|
||||
|
||||
|
||||
def parse_keychip_suffix(self, suffix: str) -> tuple[int, int]:
|
||||
year = int(suffix[0:2])
|
||||
half = int(suffix[2])
|
||||
|
|
|
@ -45,6 +45,7 @@ allnet:
|
|||
loglevel: "info"
|
||||
allow_online_updates: False
|
||||
update_cfg_folder: ""
|
||||
save_billing: True
|
||||
|
||||
billing:
|
||||
standalone: True
|
||||
|
|
Loading…
Reference in New Issue