dkanavis

money

Oct 18th, 2018
103
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.61 KB | None | 0 0
  1. #!/usr/bin/env python3.6
  2. # -*- coding: utf-8 -*-
  3.  
  4. """
  5.    Payments processor: money entrypoint. Interactive CLI application.
  6. """
  7.  
  8. import sys
  9. import re
  10. import datetime
  11. import os
  12. import traceback
  13. from typing import Callable, Any, Union
  14.  
  15. DIR = os.path.dirname(os.path.abspath(__file__))
  16. TOP_DIR = os.path.dirname(os.path.dirname(DIR))
  17. sys.path.insert(0, TOP_DIR)
  18.  
  19. from payments import get_core, processor_func, Request, Context, Income, exception_notify
  20. from payments.core.errors import ProgrammingError
  21. from payments.models.Acct import PaymentType, ServiceType
  22. from payments.models.User import User
  23. from payments.protocols.RinetAPI2 import RinetAPI, RinetAPIException
  24.  
  25.  
  26. out_charset = os.environ.get("MONEY_CHARSET", "koi8-r")
  27. error_message = "неверный ввод"
  28. L_FORMAT = "%-16s%.02f    %s %s %04d%02d%02d%02d%02d %-14.02f %.04f %s %s"
  29. validation_errors = {
  30.     "Internet-cards are forbidden": "Интернет-карты запрещены",
  31.     "Uralsib is forbidden": "Банк Уралсиб запрещен",
  32.     "Outdated service type": "Устаревший тип сервиса",
  33.     "Unsupported payment type": "Тип платежа не поддерживается",
  34.     "Unsupported service type": "Тип сервиса не поддерживается",
  35. }
  36. PTYPES_BILL = ('O',)   #  Ptypes with required bill
  37.  
  38. core = get_core()
  39. acct_sess = core.db.session("acct")
  40.  
  41.  
  42. """ >>>>>>> ARGV PARSE <<<<<<< """
  43. # Required args
  44. user = None
  45. if len(sys.argv) < 2 or len(sys.argv[1]) < 9 or sys.argv[1][:7] != "--user=":
  46.     print("First arg must be --user=user!")
  47.     sys.exit(1)
  48.  
  49. user = sys.argv[1][7:]
  50. curr_argv = 2
  51.  
  52.  
  53. # Arg functions
  54. def has_argv():
  55.     global curr_argv
  56.     return curr_argv < len(sys.argv)
  57. def get_argv():
  58.     global curr_argv
  59.     val = sys.argv[curr_argv]
  60.     curr_argv += 1
  61.     return val
  62. def rest_argv():
  63.     global curr_argv
  64.     val = " ".join(sys.argv[curr_argv:])
  65.     curr_argv = len(sys.argv)
  66.     return val
  67.  
  68.  
  69. # Optional args
  70. if has_argv() and sys.argv[curr_argv] == "--verbose":
  71.     verbosearg = True
  72.     curr_argv += 1
  73. elif has_argv() and sys.argv[curr_argv] == "--noverbose":
  74.     verbosearg = False
  75.     curr_argv += 1
  76. else:
  77.     verbosearg = core.DEVEL
  78.  
  79.  
  80. """ >>>>>>> IO HELPERS <<<<<<< """
  81. def c(s: str):
  82.     """ Convert charset """
  83.     global out_charset
  84.     return s.encode(out_charset)
  85.  
  86.  
  87. def cprint(s: str, nonl=False, file=sys.stdout):
  88.     """ Print with convertation """
  89.     try:
  90.         file.buffer.write(c(str(s)))
  91.         if not nonl:
  92.             file.buffer.write(c("\n"))
  93.         file.flush()
  94.     except BrokenPipeError:
  95.         try:
  96.             print("Broken pipe", file=sys.stderr)
  97.         except Exception:
  98.             pass
  99.         sys.exit(255)
  100.  
  101.  
  102. def read_val(message: str, converter: Callable = str, argv: bool=True) -> Any:
  103.     """
  104.        Read input value with given type-converter (or get from argument)
  105.    """
  106.     global error_message, out_charset
  107.  
  108.     from_argv = False
  109.     while True:
  110.         if argv and has_argv():
  111.             from_argv = True
  112.             raw = get_argv()
  113.         else:
  114.             cprint(message, nonl=True)
  115.             try:
  116.                 raw = sys.stdin.buffer.readline()
  117.                 raw = raw.decode(out_charset)
  118.                 raw = raw[:-1]
  119.             except (KeyboardInterrupt, EOFError):
  120.                 cprint("Exit")
  121.                 sys.exit(0)
  122.         try:
  123.             data = converter(raw)
  124.             return data
  125.         except ValueError as err:
  126.             if from_argv:
  127.                 raise
  128.             else:
  129.                 cprint("%s (%s)" % (error_message, err))
  130.  
  131.  
  132. def list_select(header: str, message: str, options: list, argv: bool=True):
  133.     """
  134.        Let user select option from list of options (or select by argument)
  135.        Each option should be formed as [value, ...aliases0-N, comment]
  136.        If options have only one element and last column of this element is
  137.            dict {"auto": "auto"} - autoselect this option
  138.    """
  139.  
  140.     if len(options) == 1 and isinstance(options[0][-1], dict) \
  141.        and options[0][-1].get("auto") == "auto":
  142.         cprint("%s%s" % (message, options[0][-2]))
  143.         return options[0][0]
  144.  
  145.     val = None
  146.     from_argv = False
  147.     if argv and has_argv():
  148.         val = get_argv().lower()
  149.         from_argv = True
  150.     else:
  151.         cprint(header)
  152.  
  153.     index = {}
  154.     for option in options:
  155.         result = option[0]
  156.         comment = option[-1]
  157.         aliases = option[1:-1]
  158.         for alias in aliases:
  159.             index[alias.lower()] = result
  160.  
  161.         aliases_str = ". ".join(aliases)
  162.         if not from_argv:
  163.             cprint("%s: %s" % (aliases_str, comment))
  164.  
  165.     while True:
  166.         if not from_argv:
  167.             val = read_val(message, str, argv).lower()
  168.         if val in index:
  169.             return index[val]
  170.         else:
  171.             if from_argv:
  172.                 raise ValueError("Value %s not found" % val)
  173.             else:
  174.                 cprint(error_message)
  175.  
  176.  
  177. def confirm(message: str, default: bool = None):
  178.     """ Ask for user's confirmation """
  179.     choices = None
  180.     if default is not None:
  181.         if default:
  182.             choices = "[Y/n]"
  183.         else:
  184.             choices = "[y/N]"
  185.     else:
  186.         choices = "[y/n]"
  187.  
  188.     message = "%s %s " % (message, choices)
  189.  
  190.     while True:
  191.         val = read_val(message)
  192.         if val:
  193.             if val == "y" or val == "Y":
  194.                 return True
  195.             elif val == "n" or val == "N":
  196.                 return False
  197.         else:
  198.             if default is not None:
  199.                 return default
  200.  
  201.  
  202. def request_error(request):
  203.     """ print request error """
  204.     if request.reason:
  205.         if request.reason in validation_errors:
  206.             reason = validation_errors[request.reason]
  207.         else:
  208.             lidx = request.reason.rfind(":")
  209.             if lidx > -1:
  210.                 lpart = request.reason[:lidx]
  211.             else:
  212.                 lpart = None
  213.             if lpart is not None and lpart in validation_errors:
  214.                 reason = validation_errors[lpart] + request.reason[lidx:]
  215.             else:
  216.                 reason = request.reason
  217.     else:
  218.         reason = "неизвестная причина"
  219.     cprint("Платеж запрещен: %s" % reason)
  220.     sys.exit(2)
  221.  
  222.  
  223. """ >>>>>>> TYPE HELPERS <<<<<<< """
  224. RE_NUM = re.compile(r'^\d+$')
  225. def word_in(val):
  226.     """ String with no spaces type helper """
  227.     val = str(val)
  228.     val = val.strip()
  229.     if " " in val:
  230.         raise ValueError("Пробелы запрещены")
  231.     return val
  232.  
  233.  
  234. def inn_in(val):
  235.     """ INN """
  236.     if len(val) != 10 and len(val) != 12:
  237.         raise ValueError("ИНН должен состоять из 10 или 12 цифр")
  238.     val = str(val)
  239.     if not RE_NUM.match(val):
  240.         raise ValueError("ИНН должен состоять из 10 или 12 цифр")
  241.     return val
  242.  
  243.  
  244. def billnum_in(val):
  245.     """ Bill number """
  246.     if len(val) != 8:
  247.         raise ValueError("Номер счёта должен состоять из 8 цифр")
  248.     val = str(val)
  249.     if not RE_NUM.match(val):
  250.         raise ValueError("Номер счёта должен состоять из 8 цифр")
  251.     return val
  252.  
  253.  
  254. RE_DATE = re.compile(r"^(\d{4})(\d{2})(\d{2})$")
  255. def date_in(empty_for_today: bool):
  256.     """ YYYYMMDD date type helper with auto-value of today option """
  257.     def inner(val):
  258.         val = str(val)
  259.         val = val.strip()
  260.         if val in ("", "-") and empty_for_today:
  261.             return datetime.datetime.now()
  262.         matches = RE_DATE.match(val)
  263.         if not matches:
  264.             raise ValueError("Wrong date format")
  265.         return datetime.datetime(
  266.             year=int(matches.group(1)),
  267.             month=int(matches.group(2)),
  268.             day=int(matches.group(3)),
  269.             hour=0,
  270.             minute=0,
  271.             second=0
  272.         )
  273.  
  274.     return inner
  275.  
  276.  
  277. """ >>>>>>> OPTIONS HELPERS <<<<<<< """
  278.  
  279. def dict_options(cls) -> list:
  280.     """ Get dict options list """
  281.     global acct_sess
  282.     query = acct_sess.query(cls).order_by(cls.id)
  283.     result = []
  284.     for obj in query:
  285.         result.append([obj.name, str(obj.id), obj.name, obj.descr])
  286.  
  287.     return result
  288.  
  289.  
  290. rinet_api = None
  291. RE_PHONELOGIN = re.compile("^(8P)(.+)$")
  292. get_bills_parameter = {
  293.     "name": "Параметр: ",
  294.     "header": "== Выберите параметр для поиска счёта ==",
  295.     "list": [
  296.         ["login", "1", "Логин"],
  297.         ["inn", "2", "ИНН"],
  298.         ["bill", "3", "Ввести номер счёта вручную"],
  299.     ]
  300. }
  301. get_bills_contract_type = {
  302.     "name": "Тип договора: ",
  303.     "header": "== Выберите тип договора для поиска счёта =="
  304. }
  305. def get_bills(in_options):
  306.     global core, rinet_api
  307.     """ Get unpaid bills for client as options list """
  308.     if rinet_api is None:
  309.         rinet_api = RinetAPI(core.config.rinet_api_token)
  310.         rinet_api.onError("throw")
  311.    
  312.     try:
  313.         login = in_options.get("login")
  314.     except KeyError:
  315.         raise ProgrammingError("Trying to get unpaid bills but no login set")
  316.  
  317.     # Ask for a parameter to search bill
  318.     reply = list_select(
  319.             get_bills_parameter["header"],
  320.             get_bills_parameter["name"],
  321.             get_bills_parameter["list"],
  322.             argv=False
  323.     )
  324.  
  325.     param = {}
  326.     auto = False
  327.     if reply == "login":
  328.         # Search bills by login
  329.         matches = RE_PHONELOGIN.match(login)
  330.         if matches:
  331.             # Phone login: let user select the contract type
  332.             foreign_login = "%s#%s" % (matches.group(1), matches.group(2))
  333.             option_list = [
  334.                 [login, "1", login, "Внутренний"],
  335.                 [foreign_login, "2", foreign_login, "Межгород"]
  336.             ]
  337.             param = {"login": list_select(
  338.                 get_bills_contract_type["header"],
  339.                 get_bills_contract_type["name"],
  340.                 option_list,
  341.                 argv=False
  342.             )}
  343.         else:
  344.             # Usual login, search by it
  345.             param = {"login": login}
  346.     elif reply == "inn":
  347.         # Search bills by INN
  348.         param = {"inn": read_val("ИНН: ", inn_in, False)}
  349.     elif reply == "bill":
  350.         # Search bills by bill number
  351.         param = {"bill_num": read_val("Номер счёта: ", billnum_in, False)}
  352.         auto = True
  353.     else:
  354.         raise ProgrammingError("Wrong bill parameter reply \"%s\"" % reply)
  355.  
  356.     # Get bills
  357.     num_tries = 0
  358.     while True:
  359.         num_tries += 1
  360.         try:
  361.             rinet_api.call("c_kassa_getbills", param)
  362.             break
  363.         except RinetAPIException as err:
  364.             cprint("Ошибка получения неоплаченных счетов: %s" % (err))
  365.             if num_tries == 3:
  366.                 cprint("Не удалось получить неоплаченные счета")
  367.                 sys.exit(1)
  368.  
  369.     # Make options list
  370.     res = []
  371.     for n, bill in enumerate(rinet_api.result):
  372.         amount = bill["bill_sum"] - bill["bill_pay"]
  373.         if amount <= 0:
  374.             continue
  375.         name = "Счёт %s" % bill["bill_num"]
  376.         for row in bill["row_set"]:
  377.             name += "\n     - %s: %d" % (row["row_descr"], row["row_sum"])
  378.         name += "\n   Итого: %d" % bill["bill_sum"]
  379.         if amount != bill["bill_sum"]:
  380.             name = "%s (не оплачено: %d руб)" % (name, amount)
  381.  
  382.         row = [{"amount": amount, "bill": bill}, str(n + 1), name]
  383.         if auto:
  384.             row.append({"auto":"auto"})
  385.         res.append(row)
  386.  
  387.     # Check if user got unpaid bills
  388.     if not res:
  389.         cprint("Счетов не найдено")
  390.         sys.exit(4)
  391.  
  392.     return res
  393.  
  394.  
  395. # Main payment options to read from stdin or argv
  396. payment_options = {
  397.     "login": {
  398.         "name": "Пользователь",
  399.         "format": word_in
  400.     },
  401.     "ptype": {
  402.         "name": "Тип платежа",
  403.         "header": "============ Типы платежа ===========",
  404.         "list": dict_options(PaymentType)
  405.     },
  406.     "stype": {
  407.         "name": "Тип сервиса",
  408.         "header": "============ Типы сервиса ===========",
  409.         "list": dict_options(ServiceType)
  410.     },
  411.     "date": {
  412.         "name": "Дата платежа [YYYYMMDD, Enter - текущая]",
  413.         "format": date_in(True)
  414.     },
  415.     "amount": {
  416.         "name": "Сумма (RUR)",
  417.         "format": float
  418.     },
  419.     "comment": {
  420.         "name": "Примечания"
  421.     }
  422. }
  423.  
  424. payment_option_bill = {   # Payment option to replace "amount" for bill-payments
  425.     "name": "Счёт",
  426.     "header": "============ Неоплаченные счета ===========",
  427.     "generator": get_bills
  428. }
  429.  
  430. edit_header = "============ Что требует замены ==========="
  431. edit_list = [[y, str(x), payment_options[y]["name"]]
  432.              for x, y in enumerate(payment_options)]
  433. edit_list.append(["!QUIT", "q", "Выход из редактирования"])
  434. edit_list.append(["!EXIT", "x", "Выход из программы"])
  435.  
  436.  
  437. def fake_user(context: Context, name: str, login: str, dlogin: Union[int,None],
  438.               service_name: str, ltype_id: int, organization_id: int = 1):
  439.     """ Make fake user object """
  440.     ltype = context.manager('user').getClientTypeByID(ltype_id)
  441.     ptype = context.manager('user').getPaymentTypeByID(1)
  442.     organization = context.manager('user').getOrganizationByID(organization_id)
  443.     return User(
  444.             id = 0,
  445.             remote_id = 0,
  446.             name = name,
  447.             first_name = "",
  448.             middle_name = "",
  449.             last_name = "",
  450.             ulogin = login,
  451.             blogin = login,
  452.             dlogin = dlogin,
  453.             service_name = service_name,
  454.             private = False,
  455.             privcorp = False,
  456.             connection_date = datetime.datetime.now().date(),
  457.             active = True,
  458.             cancelled = False,
  459.             status_id = 10,
  460.             ltype = ltype,
  461.             payment_type = ptype,
  462.             organization = organization
  463.         )
  464.  
  465.  
  466. def make_request(context, in_options):
  467.     """ Make request from input data """
  468.     global user
  469.     if in_options["ptype"] in PTYPES_BILL:
  470.         amount = in_options["amount"]["amount"]
  471.         comment = "p/o %s s4et %s" % (in_options["comment"],
  472.                                       in_options["amount"]["bill"]["bill_num"])
  473.         bill = in_options["amount"]["bill"]
  474.     else:
  475.         amount = in_options["amount"]
  476.         comment = in_options["comment"]
  477.         bill = None
  478.  
  479.     request = Request(
  480.         entrypoint_type="cli",
  481.         entrypoint_name="money",
  482.         raw_acct=in_options["login"],
  483.         amount=amount,
  484.         time=in_options["date"],
  485.         data={
  486.             "payment_type": in_options["ptype"],
  487.             "service_type": in_options["stype"],
  488.             "comment": comment,
  489.             "operator": user
  490.         }
  491.     )
  492.     if bill is not None:
  493.         request.data["bill"] = bill
  494.  
  495.     return request
  496.  
  497.  
  498. def fmt_log_request(request: Request):
  499.     res = "acct: %s, ptype: %s, stype: %s, sum: %s, date: %s" % (
  500.         request.raw_acct,
  501.         request.data['payment_type'],
  502.         request.data['service_type'],
  503.         request.amount,
  504.         request.time.strftime('%Y.%m.%d')
  505.     )
  506.     if "noreceipt" in request.data:
  507.         res = "%s %sreceipt" % (res, "no" if request.data["noreceipt"] else "")
  508.     return res
  509.  
  510.  
  511. def read_opt(option: dict, in_options: dict, argv=True):
  512.     """ Read option configured by dict """
  513.     message = "%s: " % option["name"]
  514.     if "generator" in option:
  515.         # Read generated option
  516.         options = option["generator"](in_options)
  517.         return list_select(option["header"], message, options, argv)
  518.     if "list" in option:
  519.         # Read list option
  520.         return list_select(option["header"], message, option["list"], argv)
  521.     elif "format" in option:
  522.         # Read simple option with format
  523.         return read_val(message, option["format"], argv=argv)
  524.     else:
  525.         # Read simple option without format
  526.         return read_val(message, argv=argv)
  527.  
  528.  
  529. def income_row(income: Income):
  530.     """ Generate incomes-like row from income object """
  531.     acnt = income.getAccountName()
  532.     sum_ = income.getGenericAmount()
  533.     ptype = income.payment_type.name
  534.     stype = income.service_type.name
  535.     ctime = income.creation_time
  536.     curr_sum = income.getAmount()
  537.     curr_rate = 27.0
  538.     operator = income.operator
  539.     time = income.payment_system_time
  540.     comment = income.comment
  541.     if time.date() != ctime.date():
  542.         dcomment = "%04d%02d%02d" % (time.year, time.month, time.day)
  543.         comment = "%s %s" % (dcomment, comment)
  544.  
  545.     return L_FORMAT % (
  546.                 acnt,
  547.                 sum_,
  548.                 ptype,
  549.                 stype,
  550.                 ctime.year,
  551.                 ctime.month,
  552.                 ctime.day,
  553.                 ctime.hour,
  554.                 ctime.minute,
  555.                 curr_sum,
  556.                 curr_rate,
  557.                 operator,
  558.                 comment
  559.     )
  560.  
  561. # Create logger
  562. logger = core.getLogger("money")
  563. logger.stderr_enabled = verbosearg
  564. logger.stdout_enabled = verbosearg
  565. logger.setNamedMark("_money")
  566.  
  567. def main():
  568.     """ MAIN """
  569.     global logger, payment_options, core, edit_header, verbosearg
  570.  
  571.     # Read options
  572.     in_options = {}
  573.     for idx, option in payment_options.items():
  574.         # Read option
  575.         try:
  576.             in_options[idx] = read_opt(option, in_options)
  577.         except ValueError as err:
  578.             cprint("%s: %s (%s)" % (option["name"], error_message, str(err)),
  579.                    file=sys.stderr)
  580.             sys.exit(1)
  581.         # Replace reading an amount with reading a bill for O ptype
  582.         if idx == "ptype" and in_options[idx] in PTYPES_BILL:
  583.             payment_options["amount"] = payment_option_bill
  584.             payment_options["comment"]["name"] = "Номер кассового ордера"
  585.  
  586.     # Read comment from rest argv if it's left
  587.     rest_argv_v = rest_argv()
  588.     if rest_argv_v:
  589.         in_options["comment"] = "%s %s" % (in_options["comment"], rest_argv_v)
  590.  
  591.     # Create context and bind logger
  592.     context = Context()
  593.     context.setLogger(logger)
  594.     logger.setNamedMark("U", user)
  595.  
  596.     # Create payment request and validate until it's confirmed by user
  597.     edit_mode = False
  598.     payment_request = None
  599.     while True:
  600.         # Create payment request
  601.         payment_request = make_request(context, in_options)
  602.         context.setRequest(payment_request)
  603.  
  604.         # Check request
  605.         logger.info("Validating %s" % fmt_log_request(payment_request))
  606.         processor_res = processor_func(context, checkmode=True)
  607.  
  608.         # Exit if not valid
  609.         if not processor_res:
  610.             request_error(payment_request)
  611.  
  612.         # Print payment rows
  613.         for income in payment_request.incomes:
  614.             # Print payment row
  615.             cprint(income_row(income))
  616.  
  617.         if not edit_mode:
  618.             # Ask if everyting's OK and exit loop if so
  619.             if confirm("Все ли в порядке?", default=False):
  620.                 break
  621.  
  622.         # Let user edit data
  623.         edit_mode = True
  624.  
  625.         # Show edit table
  626.         idx = list_select(edit_header, "?: ", edit_list, False)
  627.  
  628.         # Read edit table result
  629.         if idx == "!QUIT":
  630.             # Exit edit mode
  631.             edit_mode = False
  632.         elif idx == "!EXIT":
  633.             sys.exit(3)
  634.         else:
  635.             # Let user edit input option
  636.             val = in_options[idx]
  637.             if idx == "ptype" and val in PTYPES_BILL:
  638.                 cprint("Нельзя изменить тип платежа %s. Начните заново." % val)
  639.             elif idx == "login" and in_options[idx] in PTYPES_BILL:
  640.                 cprint(("Нельзя изменить логин, если тип платежа %s." % val)
  641.                        + " Начните заново.")
  642.             else:
  643.                 option = payment_options[idx]
  644.                 in_options[idx] = read_opt(option, in_options)
  645.  
  646.     # Ask whether to print a receipt if receipt marked as needed by validator
  647.     if payment_request.receipt_needed:
  648.         context.request.data["noreceipt"] = not confirm("Печатать чек?")
  649.         if context.request.data["noreceipt"]:
  650.             context.request.incomes[0].comment += " noreceipt"
  651.  
  652.     # Make payment
  653.     logger.info("Paying %s" % fmt_log_request(payment_request))
  654.     processor_res = processor_func(context)
  655.  
  656.     # Show payment rows or failure
  657.     if processor_res:
  658.         # Payment ok, show rows
  659.         for income in context.request.incomes:
  660.             row = income_row(income)
  661.             cprint("%d %s" % (income.dbid, row))
  662.     else:
  663.         # Payment failed
  664.         request_error(context.request)
  665.  
  666.  
  667. if __name__ == "__main__":
  668.     try:
  669.         main()
  670.     except Exception as err:
  671.         # Global exception handler: log, notify exception and re-raise
  672.  
  673.         try:
  674.             logger.exception(err)
  675.         except Exception as err2:
  676.             trb = traceback.format_exception(type(err2), err2, err2.__traceback__)
  677.             print("Exception in exception logger: %s" % trb, file=sys.stderr)
  678.  
  679.         try:
  680.             exception_notify(err, "money")
  681.         except Exception as err2:
  682.             trb = traceback.format_exception(type(err2), err2, err2.__traceback__)
  683.             print("Exception in exception notification: %s" % trb, file=sys.stderr)
  684.             logger.exception(err2)
  685.  
  686.         raise
Add Comment
Please, Sign In to add comment