linkaja.py 9.81 KB
import transaction
from ISO8583.ISOErrors import BitNotSet
from sqlalchemy import create_engine
from sqlalchemy.orm import (
    sessionmaker,
    scoped_session,
    )
from zope.sqlalchemy import register
from pyramid.view import (
    view_config,
    notfound_view_config,
    )
from pyramid.response import Response
from deform import ValidationFailure
from opensipkd.string import FixLength
from opensipkd.iso8583.bjb.pbb.structure import INVOICE_PROFILE
from opensipkd.iso8583.bjb.pbb.agratek.models import (
    Rpc,
    Log,
    )
from iso8583_web.scripts.tools import iso_to_dict
from iso8583_web.read_conf import web_path_conf
from iso8583_web.scripts.logger import (
    log_web_info,
    log_web_error,
    )
from . import (
    WebJob as BaseWebJob,
    View as BaseView,
    )
from pyramid_linkaja.exceptions import (
    InvoiceIdError,
    NeedPostError,
    InternalError,
    TrxTypeError,
    HostError,
    AlreadyPaidError,
    TimeoutError,
    BaseError,
    AmountError,
    BillRefNotFound,
    PaymentNotFound,
    LinkError,
    )
from pyramid_linkaja.responses import (
    InquiryResponse,
    PaymentResponse,
    get_trx_date,
    get_method,
    get_template_response,
    is_inquiry,
    is_payment,
    is_reversal,
    )
from pyramid_linkaja.decorator import csv_method
from pyramid_linkaja.structure import RENDERER
from pyramid_linkaja.form import get_form


ROUTE = 'linkaja'

conf = dict()


def get_db_session():
    return conf['db_session']


class WebJob(BaseWebJob):
    def timeout_error(self):  # override
        return TimeoutError()


def get_inquiry(data):
    DBSession = get_db_session()
    bill_ref = int(data['bill_ref'])
    q = DBSession.query(Rpc).filter_by(id=bill_ref)
    return q.first()


def get_payment(data):
    DBSession = get_db_session()
    bill_ref = int(data['bill_ref'])
    q = DBSession.query(Rpc).filter_by(
            inquiry_id=bill_ref, trx_type='022')
    q = q.order_by(Rpc.id.desc())
    return q.first()


def profile2name(profile):
    msg = [profile['Nama'].strip()]
    fields = [('Tagihan', 'Pok'), ('Denda', 'Den'), ('Discount', 'Disk')]
    for field, label in fields:
        s = profile[field].strip().lstrip('0')
        if s:
            msg.append(f'{label} {s}')
    fields = ['Nama Kelurahan', 'Nama Kecamatan', 'Lokasi']
    for field in fields:
        s = profile[field].strip()
        if s:
            msg.append(s)
    return ', '.join(msg)


def get_ok_notify(data):
    for key in conf['notification_message']:
        if data['acc_no'].find(key) == 0:
            return conf['notification_message'][key]
    return ''


class View(BaseView):
    def get_web_job_cls(self):  # Override
        return WebJob

    def not_found_error(self, hostname):  # Override
        return HostError(hostname)

    def not_running_error(self, hostname):  # Override
        msg = f'Host {hostname} belum terhubung'
        return InternalError(msg, 'Sedang offline')

    def create_iso_log(self, iso, rpc):
        conf = self.get_iso_conf()
        iso_log = Log(
            mti=iso.getMTI(),
            rpc_id=rpc.id,
            ip=conf['ip'],
            conf_name=conf['name'])
        for bit in iso.get_bit_definition():
            try:
                value = iso.getBit(bit)
            except BitNotSet:
                continue
            field = 'bit_{}'.format(str(bit).zfill(3))
            setattr(iso_log, field, value)
        try:
            data = iso.getBit(62)
            profile = FixLength(INVOICE_PROFILE)
            profile.set_raw(data)
            iso_log.bit_062_data = profile.to_dict()
        except BitNotSet:
            pass
        return iso_log

    def before_send_iso(self, data, inq, pay, iso_req):
        DBSession = get_db_session()
        web_conf = self.get_web_conf()
        row = Rpc(
            ip=self.request.client_addr,
            conf_name=web_conf['name'],
            merchant=data['merchant'],
            terminal=data['terminal'],
            trx_type=data['trx_type'],
            msisdn=data['msisdn'],
            acc_no=data['acc_no'],
            stan=iso_req.get_stan())
        row.trx_date = get_trx_date(data),
        if data.get('msg'):
            row.msg = data['msg']
        if data.get('amount'):
            row.amount = int(data['amount'])
        if not is_inquiry(data):
            row.inquiry_id = inq and inq.id or pay.inquiry_id
            row.ntb = data['trx_id']
        with transaction.manager:
            DBSession.add(row)
            DBSession.flush()
            DBSession.expunge_all()  # Agar dapat row.id
            iso_log = self.create_iso_log(iso_req, row)
            DBSession.add(iso_log)
        return row

    def after_send_iso(self, data, inq, pay, row, iso_resp):
        DBSession = get_db_session()
        iso_log = self.create_iso_log(iso_resp, row)
        iso_data = iso_to_dict(iso_resp)
        web_data = get_template_response(data)
        if iso_data[39] == '00':
            web_data['Response Code'] = '00'
            web_data['Notification Message'] = get_ok_notify(data)
            if is_inquiry(data):
                web_data['Bill Ref'] = str(row.id)
            if iso_data.get(62):
                profile = FixLength(INVOICE_PROFILE)
                profile.set_raw(iso_data[62])
                iso_log.bit_062_data = profile.to_dict()
                web_data['Biller Name'] = row.biller_name = \
                    profile2name(profile)
            web_data['Bill Amount'] = iso_data[4].lstrip('0')
            if iso_data.get(47):
                web_data['Transaction ID'] = row.ntp = iso_data[47]  # NTP
            err = None
        elif iso_data[39] in ['33', '55']:
            err = InvoiceIdError()
        elif iso_data[39] == '54':
            if is_reversal(data):
                err = PaymentNotFound()
            else:
                err = AlreadyPaidError()
        elif iso_data[39] == '51':
            err = AmountError()
        elif iso_data[39] == '91':
            err = LinkError()
        else:
            err = BaseError()
        if err:
            web_data.from_err(err)
            row.resp_msg = web_data['Notification Message']
        self.log_send(web_data.values)
        row.resp_code = web_data['Response Code']
        row.resp_msg = web_data['Notification Message']
        with transaction.manager:
            DBSession.add(row)
            DBSession.add(iso_log)
        return web_data

    def get_response(self, data):
        invoice_id = data['acc_no']
        if len(invoice_id) == 18:
            invoice_id += data['msg']
        p = dict(invoice_id=invoice_id)
        inq = pay = None
        if not is_inquiry(data):
            p['amount'] = data['amount']
            p['ntb'] = data['trx_id']
            if is_payment(data):
                inq = get_inquiry(data)
                if not inq:
                    raise BillRefNotFound()
            else:
                pay = get_payment(data)
                if not pay:
                    raise BillRefNotFound()
                p['stan'] = pay.stan
        p['db_session'] = get_db_session()
        conn = self.get_connection()
        method = get_method(data)
        iso_func = getattr(conn.job, method)
        iso_req = iso_func(p)
        row = self.before_send_iso(data, inq, pay, iso_req)
        iso_resp = self.web_job(conn, iso_req)
        return self.after_send_iso(data, inq, pay, row, iso_resp)

    @csv_method(route_name=ROUTE)
    def view_trx(self):
        self.validate()
        if not self.request.POST:
            self.log_receive(f'GET {self.request.GET}')
            raise NeedPostError()
        items = self.request.POST.items()
        self.log_receive(f'POST {dict(items)}')
        items = self.request.POST.items()
        form = get_form()
        try:
            c = form.validate(items)
        except ValidationFailure as e:
            d = e.error.asdict()
            for field, err in d.items():
                msg = f'{field} {err}'
                break
            raise InternalError(msg)
        data = dict(c)
        return self.get_response(data)
        try:
            r = self.get_response(data)
            self.log_send(r)
            return r
        except BaseError as e:
            r = get_template_response(data)
            r['Response Code'] = e.code
            r['Notification Message'] = e.message
            return r

    @notfound_view_config()
    def view_not_found(self):
        msg = f'Path {self.request.path} tidak ada'
        self.log_receive(msg, True)
        return self.request.exception


def log_prefix(request):
    web_conf = web_path_conf.get(request.path)
    name = web_conf['name']
    return f'{request.client_addr} {name} {id(request)}'


@view_config(context=BaseError)
def view_exception(exc, request):
    r = InquiryResponse()
    r['Response Code'] = exc.code
    r['Notification Message'] = exc.message
    prefix = log_prefix(request)
    msg = f'{prefix} Send {r}'
    log_web_error(msg)
    response = Response(str(r))
    response.status_int = 200
    return response


# Dipanggil read_conf.py
def init(cfg):
    conf.update(cfg)


def str2dict(s):
    r = dict()
    for token in s.split('\n'):
        s = token.strip()
        if not s:
            continue
        key, value = s.split(':')
        r[key] = value
    return r


# Dipanggil forwarder.py
def pyramid_init(config):
    config.add_renderer(RENDERER, 'pyramid_linkaja.renderer.Renderer')
    path = conf['route_path']
    config.add_route(ROUTE, path)
    log_web_info(f'LinkAja route path {path}')
    pool_size = int(conf.get('db_pool_size', 50))
    max_overflow = int(conf.get('db_max_overflow', 100))
    engine = create_engine(
        conf['db_url'], pool_size=pool_size, max_overflow=max_overflow)
    session_factory = sessionmaker(bind=engine)
    conf['db_session'] = scoped_session(session_factory)
    register(conf['db_session'])
    conf['notification_message'] = str2dict(conf['notification_message'])