#!/usr/bin/env python3 """One recipient request. Preview by default; no automatic retries or redirects.""" import argparse import json import os import stat import sys from pathlib import Path os.environ.setdefault('HISSPAY_GATEWAY', 'https://sandbox-api.hisspay.com') import client PATHS={action:'/transaction/payout/v1/beneficiaries/'+action for action in ('create','query','update')} def load(path): p=Path(path) if p.is_symlink() or not stat.S_ISREG(p.stat().st_mode) or p.stat().st_mode & 0o077: raise ValueError('Use a private regular JSON file, chmod 600') body=json.loads(p.read_text(encoding='utf-8')) if not isinstance(body,dict) or any(not isinstance(v,str) and not (k=='version' and type(v) is int) for k,v in body.items()): raise ValueError('Use strings except the integer version') if set(body)&{'appId','timestamp','nonce','sign'}: raise ValueError('Authentication belongs in headers') return body def validate(body, action): common={'operation','product','payeeId'} profile={'language','country','phone','birthdate','email','firstName','lastName','zip','city','state','address'} expected=common if action=='query' else common|profile if action=='update':expected|={'version','requestId'} if set(body)!=expected or any(isinstance(v,str) and not v.strip() for v in body.values()): raise ValueError('Unexpected or missing fields') if len(body['product'])>20:raise ValueError('Product too long') if body['operation']!=action.upper():raise ValueError('operation must match endpoint') import re if not re.fullmatch(r'[A-Za-z0-9_-]{3,60}',body['payeeId']):raise ValueError('Invalid payeeId') if action!='query': from datetime import date,datetime,timezone if body['country']!='US' or not re.fullmatch(r'\d{4}-\d{2}-\d{2}',body['birthdate']):raise ValueError('Use US recipient and ISO birthdate') born=date.fromisoformat(body['birthdate']) if not date(1900,1,1)<=born<=datetime.now(timezone.utc).date():raise ValueError('Invalid birthdate') patterns={'language': '[a-zA-Z]{2}', 'phone': '(\\+[0-9]{1,3}-)?[0-9]{4,12}', 'email': '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9]+)', 'firstName': "[A-Za-zÀ-ÿß'-][A-Za-zÀ-ÿß '-]{1,48}[A-Za-zÀ-ÿß'-]", 'lastName': "[A-Za-zÀ-ÿß'-][A-Za-zÀ-ÿß '-]{1,48}[A-Za-zÀ-ÿß'-]", 'zip': '[0-9]{5}(?:-[0-9]{4})?|[0-9]{5,10}', 'city': "[A-Za-zÀ-ÿß'&()\\-][A-Za-zÀ-ÿß '&()\\-]{0,48}[A-Za-zÀ-ÿß'&()\\-]", 'state': "[A-Za-zÀ-ÿß'&()\\-][A-Za-zÀ-ÿß '&()\\-]{0,48}[A-Za-zÀ-ÿß'&()\\-]", 'address': "[0-9A-Za-zÀ-ÿß #'(),\\-./:;°]{5,100}"} if any(not re.fullmatch(pattern,body[key]) for key,pattern in patterns.items()) or len(body['email'])>120:raise ValueError('Invalid recipient format') if action=='update' and (type(body['version']) is not int or not 0<=body['version']<=9223372036854775806 or not re.fullmatch(r'[A-Za-z0-9_-]{3,60}',body['requestId'])): raise ValueError('Invalid version or requestId') def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('action',choices=PATHS);parser.add_argument('file');parser.add_argument('--send',action='store_true') args=parser.parse_args() try: if client.GATEWAY not in ('https://sandbox-api.hisspay.com','https://transaction.hisspay.com'): raise ValueError('Unrecognized gateway') body=load(args.file) validate(body,args.action) if not args.send: print(json.dumps({'preview':True,'gateway':client.GATEWAY,'path':PATHS[args.action],'fields':list(body),'note':'No request sent; recipient data omitted'}));return 0 client.PATHS['beneficiary_'+args.action]=PATHS[args.action] status,result=client.request('beneficiary_'+args.action,body,os.environ['HISSPAY_APP_ID'],os.environ['HISSPAY_APP_SECRET']) print(json.dumps({'http':status,'response':result},ensure_ascii=False)) return 0 if status==200 and result.get('code')==0 else 1 except (ValueError,KeyError,OSError): print('NOT_CONFIRMED: query the same payeeId; do not create a replacement recipient or automatically retry an update.',file=sys.stderr);return 1 if __name__=='__main__':sys.exit(main())