Commit caa7ba2d by Owo Sugiana

Penggunaan paket pyramid-linkaja

1 parent ce7e4d5f
0.3.3 2020-09-07
0.3.3 2020-09-14
----------------
- web_client_linkaja.py ada opsi --timeout
- Init database linkaja dipindahkan ke paket opensipkd-iso8583-bjb
- Hal umum pada linkaja dipindahkan ke paket pyramid-linkaja agar semakin
modular
0.3.2 2020-08-27
----------------
......
......@@ -410,7 +410,18 @@ Dari sisi bank Agratek ini dikategorikan sebagai aggregator. Bila simulasi
aplikasi teller sebelumnya menggunakan format JSON-RPC maka kali ini akan
menggunakan format yang sudah ditentukan LinkAja. Siapkan terminal ke-3.
Modul ``linkaja`` membutuhkan database untuk pencatatan transaksi. Buatlah
database-nya terlebih dahulu. Lalu buatlah file ``test-agratek.ini``::
database-nya terlebih dahulu. Lalu buatlah file ``initdb-agratek.ini``::
[main]
module = opensipkd.iso8583.bjb.pbb.agratek
db_url = postgresql://user:pass@localhost/db
Jalankan::
$ ~/env/bin/iso8583_bjb_pbb_agratek initdb-agratek.ini
Lalu buatlah file ``test-agratek.ini``::
[loggers]
keys = root, iso8583_web
......@@ -477,14 +488,17 @@ database-nya terlebih dahulu. Lalu buatlah file ``test-agratek.ini``::
22:010
32:015
35:622011888888888888=9912?
37:000000000000
41:AGRATEK
42:LINKAJA
43:AGRATEK-LINKAJA
49:360
59:PAY
60:123
63:214
# Bit 60 diisi sesuai prefix bit 61 (Invoice ID)
bit_60 =
3271:123
3676:142
3275:120
Buat tabelnya::
......
......@@ -70,7 +70,6 @@ request_bits =
22:010
32:015
35:622011888888888888=9912?
37:000000000000
41:AGRATEK
42:LINKAJA
43:AGRATEK-LINKAJA
......
......@@ -48,7 +48,7 @@ threads = 12
[web_teller]
route_path = /rpc
host = kota_bogor
module = iso8583_web.scripts.view.jsonrpc
module = iso8583_web.scripts.views.jsonrpc
allowed_ip =
127.0.0.1
10.8.20.1
......@@ -62,8 +62,20 @@ module = opensipkd.iso8583.bjb.pbb.test_aggregator
db_url = postgresql://user:pass@localhost/db
# Routing dari 4 digit awal InvoiceID
host =
3271:kota_bogor
3275:kota_bekasi
3271:kota_bogor
3275:kota_bekasi
valid_bits =
2:622011888888888888
18:6025
22:010
32:015
35:622011888888888888=9912?
41:AGRATEK
42:LINKAJA
43:AGRATEK-LINKAJA
49:360
59:PAY
63:214
[host_kota_bogor]
ip = 127.0.0.1
......
import sys
from configparser import ConfigParser
from sqlalchemy import create_engine
from .views.linkaja.models import Base
def main(argv=sys.argv):
conf_file = argv[1]
conf = ConfigParser()
conf.read(conf_file)
for section in conf.sections():
if section.find('web_') < 0:
continue
module_name = conf.get(section, 'module')
if module_name != 'iso8583_web.scripts.views.linkaja':
continue
db_url = conf.get(section, 'db_url')
engine = create_engine(db_url)
engine.echo = True
Base.metadata.create_all(engine)
import transaction
import venusian
from ISO8583.ISOErrors import BitNotSet
from sqlalchemy import create_engine
from sqlalchemy.orm import (
......@@ -12,17 +11,13 @@ from pyramid.view import (
notfound_view_config,
)
from pyramid.response import Response
from deform import (
Form,
Button,
ValidationFailure,
)
import colander
from opensipkd.string import (
FixLength,
FullDateTimeVar,
)
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 (
......@@ -33,7 +28,7 @@ from .. import (
WebJob as BaseWebJob,
View as BaseView,
)
from .exceptions import (
from pyramid_linkaja.exceptions import (
InvoiceIdError,
NeedPostError,
InternalError,
......@@ -47,27 +42,15 @@ from .exceptions import (
PaymentNotFound,
LinkError,
)
from .structure import (
DataRequest,
from pyramid_linkaja.response import (
InquiryResponse,
PaymentResponse,
)
from .renderer import Renderer
from .models import (
Rpc,
Log,
get_trx_date,
)
ROUTE = 'linkaja'
RENDERER = 'csv'
METHOD = {
'021': 'inquiry',
'022': 'payment',
'023': 'reversal'}
METHOD_CODE = dict()
for key, value in METHOD.items():
METHOD_CODE[value] = key
conf = dict()
......@@ -81,26 +64,6 @@ class WebJob(BaseWebJob):
return TimeoutError()
def form_validator(form, value):
if value['trx_type'] not in METHOD:
raise TrxTypeError()
def get_form():
schema = DataRequest(validator=form_validator)
return Form(schema)
def date_from_str(s):
t = FullDateTimeVar()
t.set_raw(s)
return t.get_value()
def get_method(data):
return METHOD[data['trx_type']]
def get_inquiry(data):
DBSession = get_db_session()
bill_ref = int(data['bill_ref'])
......@@ -117,49 +80,6 @@ def get_payment(data):
return q.first()
def is_inquiry(data):
return data['trx_type'] == METHOD_CODE['inquiry']
def is_payment(data):
return data['trx_type'] == METHOD_CODE['payment']
def is_reversal(data):
return data['trx_type'] == METHOD_CODE['reversal']
def get_template_response(data):
if is_inquiry(data):
return InquiryResponse()
d = PaymentResponse()
d['Bill Ref'] = data['bill_ref']
return d
# view decorator
class csv_method(object):
def __init__(self, **kw):
self.kw = kw
def __call__(self, wrapped):
kw = self.kw.copy()
depth = kw.pop('_depth', 0)
def callback(context, name, ob):
config = context.config.with_package(info.module)
config.add_view(view=ob, renderer=RENDERER, **kw)
info = venusian.attach(
wrapped, callback, category='pyramid', depth=depth + 1)
if info.scope == 'class':
# ensure that attr is set if decorating a class method
kw.setdefault('attr', wrapped.__name__)
kw['_info'] = info.codeinfo # fbo action_method
return wrapped
def profile2name(profile):
msg = [profile['Nama'].strip()]
fields = [('Tagihan', 'Pok'), ('Denda', 'Den'), ('Discount', 'Disk')]
......@@ -190,7 +110,7 @@ class View(BaseView):
return HostError(hostname)
def not_running_error(self, hostname): # Override
msg = 'Host {} belum terhubung'.format(hostname)
msg = f'Host {hostname} belum terhubung'
return InternalError(msg, 'Sedang offline')
def create_iso_log(self, iso, rpc):
......@@ -228,7 +148,7 @@ class View(BaseView):
msisdn=data['msisdn'],
acc_no=data['acc_no'],
stan=iso_req.get_stan())
row.trx_date = date_from_str(data['trx_date']),
row.trx_date = get_trx_date(data),
if data.get('msg'):
row.msg = data['msg']
if data.get('amount'):
......@@ -305,6 +225,7 @@ class View(BaseView):
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)
......@@ -317,10 +238,10 @@ class View(BaseView):
def view_trx(self):
self.validate()
if not self.request.POST:
self.log_receive('GET {}'.format(self.request.GET))
self.log_receive(f'GET {self.request.GET}')
raise NeedPostError()
items = self.request.POST.items()
self.log_receive('POST {}'.format(dict(items)))
self.log_receive(f'POST {dict(items)}')
items = self.request.POST.items()
form = get_form()
try:
......@@ -328,7 +249,7 @@ class View(BaseView):
except ValidationFailure as e:
d = e.error.asdict()
for field, err in d.items():
msg = '{} {}'.format(field, err)
msg = f'{field} {err}'
break
raise InternalError(msg)
data = dict(c)
......@@ -345,7 +266,7 @@ class View(BaseView):
@notfound_view_config()
def view_not_found(self):
msg = 'Path {} tidak ada'.format(self.request.path)
msg = f'Path {self.request.path} tidak ada'
self.log_receive(msg, True)
return self.request.exception
......@@ -353,8 +274,7 @@ class View(BaseView):
def log_prefix(request):
web_conf = web_path_conf.get(request.path)
name = web_conf['name']
return '{} {} {}'.format(
request.client_addr, name, id(request))
return f'{request.client_addr} {name} {id(request)}'
@view_config(context=BaseError)
......@@ -388,9 +308,10 @@ def str2dict(s):
# Dipanggil forwarder.py
def pyramid_init(config):
config.add_renderer(RENDERER, 'iso8583_web.scripts.views.linkaja.Renderer')
config.add_route(ROUTE, conf['route_path'])
log_web_info('LinkAja route path {}'.format(conf['route_path']))
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(
......
class BaseError(Exception):
code = '49'
message = 'Ada kesalahan yang belum dipahami'
class NeedPostError(BaseError):
code = '40'
message = 'HTTP Request harus POST'
class TrxTypeError(BaseError):
code = '41'
message = 'trx_type tidak dikenal'
class InternalError(BaseError):
def __init__(self, orig_msg, msg='Ada kesalahan internal'):
self.orig_msg = orig_msg
self.message = msg
class TimeoutError(BaseError):
code = '42'
message = 'Timeout'
class InvoiceIdError(BaseError):
code = '43'
message = 'acc_no tidak ditemukan'
class HostError(BaseError):
code = '44'
def __init__(self, hostname):
self.hostname = hostname
self.message = 'Host {} tidak terdaftar'.format(hostname)
class AlreadyPaidError(BaseError):
code = '45'
message = 'Memang sudah lunas'
class AmountError(BaseError):
code = '46'
message = 'Jumlah pembayaran tidak sesuai tagihan'
class BillRefNotFound(BaseError):
code = '47'
message = 'bill_ref tidak ditemukan'
class PaymentNotFound(BaseError):
code = '48'
message = 'Belum ada pembayaran'
class LinkError(BaseError):
code = '49'
message = 'Koneksi terputus'
from sqlalchemy import (
Column,
Integer,
Float,
DateTime,
String,
ForeignKey,
func,
JSON,
)
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Rpc(Base):
__tablename__ = 'linkaja_trx'
id = Column(Integer, primary_key=True) # Bill Ref
created = Column(
DateTime(timezone=True), nullable=False, server_default=func.now())
ip = Column(String(15), nullable=False)
conf_name = Column(String(16), nullable=False)
merchant = Column(String(100), nullable=False)
terminal = Column(String(100), nullable=False)
trx_type = Column(String(3), nullable=False)
msisdn = Column(String(20), nullable=False)
# Invoice ID = SPPT ID di PBB
acc_no = Column(String(64), nullable=False)
msg = Column(String(100))
trx_date = Column(DateTime(timezone=True), nullable=False)
# Dari inquiry response
amount = Column(Float)
# Dari inquiry response (Bill Ref),
# diisi saat payment request (bill_ref)
inquiry_id = Column(Integer, ForeignKey('linkaja_trx.id'))
# Dari payment request (trx_id)
ntb = Column(String(32))
# Penerjemahan bit 39
resp_code = Column(String(2))
# Penjelasan resp_code untuk pelanggan
resp_msg = Column(String(100))
# Penjelasan resp_code untuk audit sistem
resp_orig_msg = Column(String(100))
# Nama wajib pajak
biller_name = Column(String(100))
# Dari payment response (Transaction ID)
ntp = Column(String(32))
# Dari bit 11, dibutuhkan untuk reversal
stan = Column(String(6))
class Log(Base):
__tablename__ = 'log_iso'
id = Column(Integer, primary_key=True)
created = Column(
DateTime(timezone=True), nullable=False, server_default=func.now())
rpc_id = Column(Integer, ForeignKey(Rpc.id), nullable=False)
ip = Column(String(15))
conf_name = Column(String(16), nullable=False)
mti = Column(String(4), nullable=False)
bit_002 = Column(String(99))
bit_003 = Column(String(6))
bit_004 = Column(String(12))
bit_007 = Column(String(10))
bit_011 = Column(String(6))
bit_012 = Column(String(6))
bit_013 = Column(String(4))
bit_015 = Column(String(4))
bit_018 = Column(String(4))
bit_022 = Column(String(3))
bit_032 = Column(String(4))
bit_033 = Column(String(10))
bit_035 = Column(String(99))
bit_037 = Column(String(12))
bit_039 = Column(String(2))
bit_041 = Column(String(8))
bit_042 = Column(String(15))
bit_043 = Column(String(40))
bit_047 = Column(String(99))
bit_048 = Column(String(99))
bit_049 = Column(String(3))
bit_059 = Column(String(16))
bit_060 = Column(String(3))
bit_061 = Column(String(22))
bit_062 = Column(String(512))
bit_063 = Column(String(255))
bit_102 = Column(String(32))
bit_107 = Column(String(8))
bit_062_data = Column(JSON)
class Renderer(object):
def __init__(self, info):
pass
def __call__(self, value, system):
''' value bertipe DataResponse '''
request = system.get('request')
if request is not None:
response = request.response
ct = response.content_type
if ct == response.default_content_type:
response.content_type = 'text/csv'
return str(value)
import colander
from opensipkd.string.row import Row
from .exceptions import InternalError
INQUIRY_RESP_FIELDS = (
'Response Code',
'Biller Name',
'Bill Amount',
'Bill Ref', # STAN
'Notification Message')
PAYMENT_RESP_FIELDS = (
'Response Code',
'Transaction ID', # NTP
'Bill Ref', # STAN
'Notification Message')
class DataRequest(colander.Schema):
merchant = colander.SchemaNode(colander.String())
terminal = colander.SchemaNode(colander.String())
pwd = colander.SchemaNode(colander.String())
trx_type = colander.SchemaNode(colander.String())
msisdn = colander.SchemaNode(colander.String())
acc_no = colander.SchemaNode(colander.String())
msg = colander.SchemaNode(colander.String(), missing=colander.drop)
trx_date = colander.SchemaNode(colander.String())
amount = colander.SchemaNode(colander.String(), missing=colander.drop)
# Saat payment dan reversal, diperoleh dari inquiry response
bill_ref = colander.SchemaNode(colander.String(), missing=colander.drop)
# Saat payment dan reversal, dibuat saat payment request (NTB)
trx_id = colander.SchemaNode(colander.String(), missing=colander.drop)
class InquiryResponse(Row):
def __init__(self):
super().__init__(INQUIRY_RESP_FIELDS)
def __setitem__(self, name, value):
if value.find(':') > -1:
msg = 'Ada titik dua pada {} yaitu {}'.format(name, value)
raise InternalError(msg)
Row.__setitem__(self, name, value)
def __str__(self):
return self.to_str()
def to_str(self):
return ':'.join(list(self))
def from_err(self, err):
self.values['Response Code'] = err.code
self.values['Notification Message'] = err.message
def from_raw(self, raw):
t = raw.split(':')
i = -1
for fieldname in self.fieldnames:
i += 1
if not t[i:i+1]:
return
self.values[fieldname] = t[i]
class PaymentResponse(InquiryResponse):
def __init__(self):
Row.__init__(self, PAYMENT_RESP_FIELDS)
......@@ -31,9 +31,10 @@ requires = [
'pyramid_mailer',
'requests',
'pyramid_rpc',
'opensipkd-base @ git+https://git.opensipkd.com/sugiana/opensipkd-base',
'opensipkd-iso8583 @ git+https://git.opensipkd.com/sugiana/opensipkd-iso8583',
'opensipkd-hitung @ git+https://git.opensipkd.com/sugiana/opensipkd-hitung',
'opensipkd-base @ git+https://git.opensipkd.com/sugiana/opensipkd-base.git',
'opensipkd-iso8583 @ git+https://git.opensipkd.com/sugiana/opensipkd-iso8583.git',
'opensipkd-hitung @ git+https://git.opensipkd.com/sugiana/opensipkd-hitung.git',
'opensipkd-hitung @ git+https://git.opensipkd.com/sugiana/pyramid-linkaja.git',
]
......@@ -75,7 +76,6 @@ setup(
'iso8583_web_client = iso8583_web.scripts.web_client:main',
'iso8583_web_client_linkaja = iso8583_web.scripts.web_client_linkaja:main',
'initialize_iso8583_web_db = iso8583_web.scripts.initialize_db:main',
'init_db_linkaja = iso8583_web.scripts.init_db_linkaja:main',
]
},
)
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!