From 492a4b977a669bb16e4952aba7d00b5951572b0e Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Thu, 9 Feb 2017 17:29:35 +0100 Subject: [PATCH 01/14] Initial version for Indico v2 Co-authored-by: Claas Faber --- payment_sixpay/.gitignore | 180 +++++ payment_sixpay/LICENSE | 674 ++++++++++++++++++ payment_sixpay/README.md | 54 ++ payment_sixpay/build_docs.sh | 23 + payment_sixpay/docs/Makefile | 20 + payment_sixpay/docs/conf.py | 191 +++++ payment_sixpay/docs/index.rst | 99 +++ .../source/api/indico_sixpay.blueprint.rst | 7 + .../docs/source/api/indico_sixpay.plugin.rst | 7 + .../api/indico_sixpay.request_handlers.rst | 7 + .../docs/source/api/indico_sixpay.rst | 18 + .../docs/source/api/indico_sixpay.utility.rst | 7 + payment_sixpay/docs/source/api/modules.rst | 7 + payment_sixpay/docs/source/changelog.rst | 35 + payment_sixpay/docs/source/configuration.rst | 121 ++++ payment_sixpay/docs/source/design.rst | 24 + .../docs/source/images/uml/transaction.svg | 58 ++ payment_sixpay/docs/source/installation.rst | 59 ++ .../docs/source/uml/transaction.uml | 45 ++ payment_sixpay/indico_sixpay/__about__.py | 54 ++ payment_sixpay/indico_sixpay/__init__.py | 17 + payment_sixpay/indico_sixpay/blueprint.py | 38 + payment_sixpay/indico_sixpay/plugin.py | 283 ++++++++ .../indico_sixpay/request_handlers.py | 253 +++++++ .../templates/event_payment_form.html | 16 + payment_sixpay/indico_sixpay/utility.py | 74 ++ payment_sixpay/setup.py | 42 ++ 27 files changed, 2413 insertions(+) create mode 100644 payment_sixpay/.gitignore create mode 100644 payment_sixpay/LICENSE create mode 100644 payment_sixpay/README.md create mode 100755 payment_sixpay/build_docs.sh create mode 100644 payment_sixpay/docs/Makefile create mode 100644 payment_sixpay/docs/conf.py create mode 100644 payment_sixpay/docs/index.rst create mode 100644 payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst create mode 100644 payment_sixpay/docs/source/api/indico_sixpay.plugin.rst create mode 100644 payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst create mode 100644 payment_sixpay/docs/source/api/indico_sixpay.rst create mode 100644 payment_sixpay/docs/source/api/indico_sixpay.utility.rst create mode 100644 payment_sixpay/docs/source/api/modules.rst create mode 100644 payment_sixpay/docs/source/changelog.rst create mode 100644 payment_sixpay/docs/source/configuration.rst create mode 100644 payment_sixpay/docs/source/design.rst create mode 100644 payment_sixpay/docs/source/images/uml/transaction.svg create mode 100644 payment_sixpay/docs/source/installation.rst create mode 100644 payment_sixpay/docs/source/uml/transaction.uml create mode 100644 payment_sixpay/indico_sixpay/__about__.py create mode 100644 payment_sixpay/indico_sixpay/__init__.py create mode 100644 payment_sixpay/indico_sixpay/blueprint.py create mode 100644 payment_sixpay/indico_sixpay/plugin.py create mode 100644 payment_sixpay/indico_sixpay/request_handlers.py create mode 100644 payment_sixpay/indico_sixpay/templates/event_payment_form.html create mode 100644 payment_sixpay/indico_sixpay/utility.py create mode 100644 payment_sixpay/setup.py diff --git a/payment_sixpay/.gitignore b/payment_sixpay/.gitignore new file mode 100644 index 0000000..87f6d35 --- /dev/null +++ b/payment_sixpay/.gitignore @@ -0,0 +1,180 @@ + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* +### macOS template +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Windows template +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/build +docs/_build +docs/_templates +docs/_static + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + diff --git a/payment_sixpay/LICENSE b/payment_sixpay/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/payment_sixpay/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/payment_sixpay/README.md b/payment_sixpay/README.md new file mode 100644 index 0000000..78377b3 --- /dev/null +++ b/payment_sixpay/README.md @@ -0,0 +1,54 @@ +# Indico 2 EPayment plugin for SIX Payment Services + +Plugin for the Indico 2 event/conference management system, enabling support for SIX Payment Service. +This enables EPayment for users via the SixPay Saferpay Payment Page in conferences and other events. + +Please see the [documentation](http://indico-sixpay.readthedocs.io/en/latest/) for details on installation, usage and maintenance. + +## Overview + +If the plugin is enabled, event participants can select the ``SixPay`` payment method during the EPayment checkout. +Payment is performed via the **Saferpay Payment Page**, an external service provided by SIX Payment Services. +The plugin handles the user interaction inside Indico, and the secure, asynchronous transaction with SIX Payment Services. + +*This is plugin supports Indico 2.0.* +*The legacy plugin for Indico 1.2 is [hosted on github](https://github.com/maxfischer2781/indico_sixpay/tree/indico-1.2).* + +## Installation + +The plugin can be installed using standard Python package managers. +Note that at least `indico` 2.0 is required, and will be installed if it is missing. + +**Note**: The `indico_sixpay` plugin must be installed for the python version running `indico`. + +### Release Version + +The latest release version is available for the default python package managers. +You can directly install the module using `pip`: + + pip install indico_sixpay + +This can also be used to upgrade to a newer version: + + pip install indico_sixpay --upgrade + +### Latest Version + +Download this repository to any host running indico. +Install it by running: + + python setup.py install + +After reloading the EPayment plugin in the Indico Admin panel, you can enable the SixPay service. + +## Contributing, Feedback and Bug Reports + +This project is hosted on [github](https://github.com/maxfischer2781/indico_sixpay). +If you encounter any bugs or missing features, please use the [bug tracker](https://github.com/maxfischer2781/indico_sixpay/issues) or submit a [pull request](https://github.com/maxfischer2781/indico_sixpay/pulls). + +## Disclaimer + +This plugin is in no way endorsed, supported or provided by SIX, Indico, KIT or any other service, provider or entity. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/payment_sixpay/build_docs.sh b/payment_sixpay/build_docs.sh new file mode 100755 index 0000000..d11e7ff --- /dev/null +++ b/payment_sixpay/build_docs.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Perform a complete build of the documentation +set -e + +LIB_NAME=indico_sixpay +DOCS_DIR=docs + +cd ${DOCS_DIR} +# cleanup backed files +touch source/api/dummy +rm source/api/* +if which plantuml >/dev/null +then + echo "Building UML images..." + touch source/images/uml/dummy + rm source/images/uml/* + plantuml -tsvg -o ../images/uml/ source/uml/*.uml +fi + +# sphinx build +sphinx-apidoc --module-first --separate --output-dir=source/api ../${LIB_NAME} --force && \ +python2 $(which sphinx-build) -b html -d build/doctrees . build/html/ && \ +open build/html/index.html diff --git a/payment_sixpay/docs/Makefile b/payment_sixpay/docs/Makefile new file mode 100644 index 0000000..2beb2ea --- /dev/null +++ b/payment_sixpay/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = chainlet +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/payment_sixpay/docs/conf.py b/payment_sixpay/docs/conf.py new file mode 100644 index 0000000..e000baa --- /dev/null +++ b/payment_sixpay/docs/conf.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# indico_sixpay documentation build configuration file, created by +# sphinx-quickstart on Wed Feb 22 14:45:32 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +import sphinx.ext.autodoc as autodoc +# sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from indico_sixpay import __about__ + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.imgmath', + 'sphinx.ext.githubpages', + 'sphinx.ext.graphviz', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = __about__.__title__ +copyright = __about__.__copyright__ +author = __about__.__author__ + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = __about__.__version__ +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'chainletdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'chainlet.tex', 'chainlet Documentation', + 'Max Fischer', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'chainlet', 'chainlet Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'chainlet', 'chainlet Documentation', + author, 'chainlet', 'One line description of project.', + 'Miscellaneous'), +] + +# plugin and extensions +intersphinx_mapping = {'https://docs.python.org/3/': None} + + +def skip_pickle_inject(app, what, name, obj, skip, options): + """skip global wrapper._raw_slave names used only for pickle support""" + if name.endswith('._raw_slave'): + return True + return None + + +def wraplet_signature(app, what, name, obj, options, signature, return_annotation): + """have wrapplets use the signature of the slave""" + try: + wrapped = obj._raw_slave + except AttributeError: + return None + else: + slave_argspec = autodoc.getargspec(wrapped) + slave_signature = autodoc.formatargspec(obj, *slave_argspec) + return (slave_signature, return_annotation) + + +def setup(app): + app.connect('autodoc-skip-member', skip_pickle_inject) + app.connect('autodoc-process-signature', wraplet_signature) diff --git a/payment_sixpay/docs/index.rst b/payment_sixpay/docs/index.rst new file mode 100644 index 0000000..8da10c6 --- /dev/null +++ b/payment_sixpay/docs/index.rst @@ -0,0 +1,99 @@ +================================================== +``indico_sixpay`` - SIX EPayment Plugin for Indico +================================================== + +.. image:: https://readthedocs.org/projects/indico_sixpay/badge/?version=latest + :target: http://indico-sixpay.readthedocs.io/en/latest/?badge=latest + :alt: Documentation + +.. image:: https://img.shields.io/pypi/v/indico_sixpay.svg + :alt: Available on PyPI + :target: https://pypi.python.org/pypi/indico_sixpay/ + +.. image:: https://img.shields.io/github/license/maxfischer2781/indico_sixpay.svg + :alt: License + :target: https://github.com/maxfischer2781/indico_sixpay/blob/master/LICENSE + +.. image:: https://img.shields.io/github/commits-since/maxfischer2781/indico_sixpay/v2.0.1.svg + :alt: Repository + :target: https://github.com/maxfischer2781/indico_sixpay/tree/master + +.. toctree:: + :maxdepth: 1 + :caption: Subtopics Overview: + + source/installation + source/configuration + source/changelog + source/design + Module Index + +The :py:mod:`indico_sixpay` adds an EPayment option for +the *SIX Payment Services* provider +to the *Indico* event management system. + +Overview +-------- + +If the plugin is enabled, event participants can select the ``SixPay`` payment method during the EPayment checkout. +Payment is performed via the **Saferpay Payment Page**, an external service provided by *SIX Payment Services*. +The plugin handles the user interaction inside Indico, and the secure, asynchronous transaction with SIX Payment Services. + +The plugin must be installed for an entire Indico instance. +It can be enabled and configured for the entire instance and per individual event. +Note that a valid account with *SIX Payment Services* is required to receive payments. + +The plugin follows the +`Saferpay Payment Page `_ +specification version ``5.1``. + +*This plugin supports Indico 2.0.* +*The legacy plugin for Indico 1.2 is hosted on* `github `_. + +Quick Guide +----------- + +To enable the plugin, it must be installed for the python version running ``indico``. + +.. code:: bash + + python -m pip install indico_sixpay + +Once installed, it can be enabled in the administrator and event settings. +Configuration uses the same options for global defaults and event specific overrides. + +Usage Notes +----------- + +The plugin relies on the ISO 4217 standard for currency conversions. +Since they are not properly covered by the standard, the currencies ``MGA`` and ``MRU`` cannot be used for payments. + +Contributing and Feedback +------------------------- + +The project is hosted on `github `_. +Feedback, pull requests and other contributions are always welcome. +If you have issues or suggestions, check the issue tracker: |issues| + +Disclaimer +---------- + +This plugin is in no way endorsed, supported or provided by SIX, Indico or any other service, provider or entity. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +---------- + +.. |issues| image:: https://img.shields.io/github/issues-raw/maxfischer2781/indico_sixpay.svg + :target: https://github.com/maxfischer2781/indico_sixpay/issues + :alt: Open Issues + +Documentation built from ``indico_sixpay`` |version| at |today|. diff --git a/payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst b/payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst new file mode 100644 index 0000000..5698cb4 --- /dev/null +++ b/payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst @@ -0,0 +1,7 @@ +indico\_sixpay\.blueprint module +================================ + +.. automodule:: indico_sixpay.blueprint + :members: + :undoc-members: + :show-inheritance: diff --git a/payment_sixpay/docs/source/api/indico_sixpay.plugin.rst b/payment_sixpay/docs/source/api/indico_sixpay.plugin.rst new file mode 100644 index 0000000..a58facb --- /dev/null +++ b/payment_sixpay/docs/source/api/indico_sixpay.plugin.rst @@ -0,0 +1,7 @@ +indico\_sixpay\.plugin module +============================= + +.. automodule:: indico_sixpay.plugin + :members: + :undoc-members: + :show-inheritance: diff --git a/payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst b/payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst new file mode 100644 index 0000000..7f6dc90 --- /dev/null +++ b/payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst @@ -0,0 +1,7 @@ +indico\_sixpay\.request\_handlers module +======================================== + +.. automodule:: indico_sixpay.request_handlers + :members: + :undoc-members: + :show-inheritance: diff --git a/payment_sixpay/docs/source/api/indico_sixpay.rst b/payment_sixpay/docs/source/api/indico_sixpay.rst new file mode 100644 index 0000000..d42503a --- /dev/null +++ b/payment_sixpay/docs/source/api/indico_sixpay.rst @@ -0,0 +1,18 @@ +indico\_sixpay package +====================== + +.. automodule:: indico_sixpay + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + indico_sixpay.blueprint + indico_sixpay.plugin + indico_sixpay.request_handlers + indico_sixpay.utility + diff --git a/payment_sixpay/docs/source/api/indico_sixpay.utility.rst b/payment_sixpay/docs/source/api/indico_sixpay.utility.rst new file mode 100644 index 0000000..dfdc9b3 --- /dev/null +++ b/payment_sixpay/docs/source/api/indico_sixpay.utility.rst @@ -0,0 +1,7 @@ +indico\_sixpay\.utility module +============================== + +.. automodule:: indico_sixpay.utility + :members: + :undoc-members: + :show-inheritance: diff --git a/payment_sixpay/docs/source/api/modules.rst b/payment_sixpay/docs/source/api/modules.rst new file mode 100644 index 0000000..1f26b88 --- /dev/null +++ b/payment_sixpay/docs/source/api/modules.rst @@ -0,0 +1,7 @@ +indico_sixpay +============= + +.. toctree:: + :maxdepth: 4 + + indico_sixpay diff --git a/payment_sixpay/docs/source/changelog.rst b/payment_sixpay/docs/source/changelog.rst new file mode 100644 index 0000000..0b43398 --- /dev/null +++ b/payment_sixpay/docs/source/changelog.rst @@ -0,0 +1,35 @@ ++++++++++ +Changelog ++++++++++ + + +v2.0.2 -- 2018-05-23 +-------------------- + + * Default SaferPay URL includes a trailing slash to form correct URLs. + +v2.0.1 -- 2018-03-01 +-------------------- + + * event settings override global settings (`issue #6 `_) + +v2.0.0 -- 2018-02-08 +-------------------- + + * public release for Indico 2.0 + +v1.2.2 -- 2017-07-10 +-------------------- + + * internal identifier for transactions is configurable + * expanded order description placeholders + +v1.2.1 -- 2017-03-14 +-------------------- + + * bugfix for duplicate transaction verification + +v1.2.0 -- 2017-03-13 +-------------------- + + * public release for Indico 1.2 diff --git a/payment_sixpay/docs/source/configuration.rst b/payment_sixpay/docs/source/configuration.rst new file mode 100644 index 0000000..9367f7e --- /dev/null +++ b/payment_sixpay/docs/source/configuration.rst @@ -0,0 +1,121 @@ +Plugin Configuration +==================== + +The plugin must be installed for an entire Indico instance. +It can be enabled and configured for the entire instance and per individual event. +Both levels have the same configuration options: +The global settings act as a default, and are overridden by event specific settings. + +Configuration Options +--------------------- + +**SixPay Saferpay URL** + + The URL to contact the Six Payment Service. + Use the default ``https://www.saferpay.com/hosting/`` for any transaction. + For testing, use the ``https://test.saferpay.com/hosting/`` test service. + + You should generally *not* change this, unless you want to test the plugin. + If the official saferpay URL changes, please submit an `issue ticket`_. + +**Account ID** + + The ID of your Saferpay account, a number containing slashes. + For testing, use the ID ``401860-17795278``. + + This ID is provided to you by Six Payment Services. + +**Order Description** [80 characters] + + The description of each order in a human readable way. + This description is presented to the registrant during the transaction with SixPay. + + This field is limited to 80 characters, after any placeholders are filled in. + The suggested length is 50 characters. + The default description uses the registrant name and event title. + +**Order Identifier** [80 characters] + + The identifier of each order for further processing. + This identifier is used internally and in your own billing. + + This field is stripped of whitespace and limited to 80 characters, after any placeholders are filled in. + Note that auxiliary services, e.g. for billing, may limit this information to 12 characters. + +**Notification Mail** + + Mail address to receive notifications of transactions. + This is independent of Indico's own payment notifications. + +Format Placeholders +------------------- + +The **Order Description/Identifier** settings allow for placeholders. +These are dynamically filled in for each event and registrant. + +``{user_id}`` [`231`] + + Numerical identifier of the user/registrant. + This is unique per event, but not globally unique. + +``{user_name}`` [`Jane Doe`] + + Full name of the user/registrant. + Use `` `` format. + +``{user_firstname}`` [`Jane`] + + First name of the user/registrant. + +``{user_lastname}`` [`Doe`] + + Last name of the user/registrant. + +``{event_id}`` [`18`] + + Numerical identifier of the event. + This is globally unique. + +``{event_title}`` [`My Conference`] + + Full title of the event. + +``{eventuser_id}`` [`e18u231`] + + A globally unique identifier for both the event and user. + +``{registration_title}`` [`Early Bird`] + + The title of the registration, as shown by Indico. + +Placeholders use the `Format String Syntax`_ of Python. +For example, ``{event_title:.6}`` is replaced with the first six characters of the event title. + +Note that both fields taking placeholders have a maximum size. +Since a template cannot be validated exactly, size validation assumes a reasonably terse input. +In practice, fields may be silently shortened after formatting with long input. + +Placeholder Examples +^^^^^^^^^^^^^^^^^^^^ + +Below are some examples for use as **Order Description** and **Order Identifier**: + +===================================================== ==================================== +Format Template Example Output +===================================================== ==================================== + **Order Description** +------------------------------------------------------------------------------------------ +``{event_title} (RegNr. {user_id})`` My Conference (RegNr. 231) +``{event_title}: {user_name} ({registration_title})`` My Conference: Jane Doe (Early Bird) +``{event_title} ({registration_title})`` My Conference (Early Bird) +----------------------------------------------------- ------------------------------------ + **Order Identifier** +------------------------------------------------------------------------------------------ +``{eventuser_id}-{user_firstname:.1}{user_lastname}`` e18u231-JDoe +``{event_title:.7} {eventuser_id}`` My Conf e18u231 +===================================================== ==================================== + + +.. _issue ticket: https://github.com/maxfischer2781/indico_sixpay/pulls + +.. _Format String Syntax: https://docs.python.org/3/library/string.html#formatstrings diff --git a/payment_sixpay/docs/source/design.rst b/payment_sixpay/docs/source/design.rst new file mode 100644 index 0000000..7d079a3 --- /dev/null +++ b/payment_sixpay/docs/source/design.rst @@ -0,0 +1,24 @@ +Implementation Overview +======================= + +The plugin follows the *Six Payment Services Payment Page* +`Specification Version 5.1 `_. +It implements the *Saferpay https interface* (Section 4 of the Specification), +but has to tie it into the Indico transaction flow. + +================= ================================================================= =============================== +HTTP API Plugin Component Implementation +================= ================================================================= =============================== +CreatePayInit :py:class:`~indico_sixpay.plugin.SixpayPaymentPlugin` ``_get_transaction_parameters`` + ``_get_payment_url`` +VerifyPayConfirm :py:class:`~indico_sixpay.request_handlers.SixPayResponseHandler` ``_verify_signature`` +CreatePayComplete :py:class:`~indico_sixpay.request_handlers.SixPayResponseHandler` ``_confirm_transaction`` +================= ================================================================= =============================== + +Payment Procedure +----------------- + +See below for an overview of the payment procedure: + +.. image:: images/uml/transaction.svg + :alt: UML of transaction diff --git a/payment_sixpay/docs/source/images/uml/transaction.svg b/payment_sixpay/docs/source/images/uml/transaction.svg new file mode 100644 index 0000000..e4c1f13 --- /dev/null +++ b/payment_sixpay/docs/source/images/uml/transaction.svg @@ -0,0 +1,58 @@ +Indico ServiceSaferpayUserUserIndico ServerIndico ServerSixPay PluginSixPay PluginSixPaySixPaySaferpayPayment Requestrequest paymentpayment detailsSaferpaysaferpay urlpayment pagePayment Transactionsaferpay urlresultvalidate resultconfirmationvalidate detailsconfirm paymentalt[success]success[failure]failure[cancel]cancel \ No newline at end of file diff --git a/payment_sixpay/docs/source/installation.rst b/payment_sixpay/docs/source/installation.rst new file mode 100644 index 0000000..f3a79fb --- /dev/null +++ b/payment_sixpay/docs/source/installation.rst @@ -0,0 +1,59 @@ +Installation +============ + +The plugin can be installed using standard Python package managers. +To enable the plugin, it must be added to the configuration file of indico. + +.. seealso:: The official `Indico Plugin Installation Guide`_. + +Note that at least ``indico`` 2.0 is required, and will be installed automatically if it is missing. + +Installing the package +---------------------- + +The ``indico_sixpay`` plugin must be installed for the python version running ``indico``. +With a standard indico installation, you must activate the indico python virtual environment first. + +.. code:: bash + + su - indico + source ~/.venv/bin/activate + +The latest release version is available for the default python package managers. +You can directly install the module using ``pip``: + +.. code:: bash + + pip install indico_sixpay + +This can also be used to upgrade to a newer version: + +.. code:: bash + + pip install indico_sixpay --upgrade + +Enabling the package +-------------------- + +All plugins must be enabled in indico's configuration file. +By default, the configuration is located in ``/opt/indico/etc/indico.conf``. + +.. code:: python + + PLUGINS = {'payment_sixpay'} + +Note that if you need multiple plugins, you must all include them in the set of ``PLUGINS``: + +.. code:: python + + PLUGINS = {'payment_manual', 'payment_paypal', 'payment_sixpay'} + +After changing the configuration, trigger a reload of the indico services. +Issue the following commands as ``root``: + +.. code:: bash + + touch ~/web/indico.wsgi + systemctl restart indico-celery.service + +.. _Indico Plugin Installation Guide: https://docs.getindico.io/en/latest/installation/plugins/ diff --git a/payment_sixpay/docs/source/uml/transaction.uml b/payment_sixpay/docs/source/uml/transaction.uml new file mode 100644 index 0000000..cff90a0 --- /dev/null +++ b/payment_sixpay/docs/source/uml/transaction.uml @@ -0,0 +1,45 @@ +@startuml +actor User + +box "Indico Service" + participant "Indico Server" as Indico + participant "SixPay Plugin" as Plugin +end box + +== Payment Request== + User -> Indico: request payment + activate Indico + Indico -> Plugin + activate Plugin + Plugin -> SixPay: payment details + activate SixPay + create participant Saferpay + SixPay -> Saferpay + Plugin <-- SixPay: saferpay url + deactivate SixPay + Indico <- Plugin + deactivate Plugin + User <- Indico: payment page + deactivate Indico +... + +== Payment Transaction == +User -> Saferpay: saferpay url +activate Saferpay + Plugin <<- Saferpay: result + activate Plugin + Plugin -> SixPay: validate result + Plugin <- SixPay: confirmation + Plugin -> Plugin: validate details + Indico <- Plugin: confirm payment + deactivate Plugin + User <- Saferpay +destroy Saferpay +alt success + User -> Plugin: success +else failure + User -> Plugin: failure +else cancel + User -> Plugin: cancel +end +@enduml diff --git a/payment_sixpay/indico_sixpay/__about__.py b/payment_sixpay/indico_sixpay/__about__.py new file mode 100644 index 0000000..f45b463 --- /dev/null +++ b/payment_sixpay/indico_sixpay/__about__.py @@ -0,0 +1,54 @@ +""" +++++++++++++++++++++++++++++++++++++++++++++++++++ +``indico_sixpay`` - SIX EPayment Plugin for Indico +++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. image:: https://readthedocs.org/projects/indico_sixpay/badge/?version=latest + :target: http://indico-sixpay.readthedocs.io/en/latest/?badge=latest + :alt: Documentation + +.. image:: https://img.shields.io/pypi/v/indico_sixpay.svg + :alt: Available on PyPI + :target: https://pypi.python.org/pypi/indico_sixpay/ + +.. image:: https://img.shields.io/github/license/maxfischer2781/indico_sixpay.svg + :alt: License + :target: https://github.com/maxfischer2781/indico_sixpay/blob/master/LICENSE + +.. image:: https://img.shields.io/github/commits-since/maxfischer2781/indico_sixpay/v2.0.0.svg + :alt: Repository + :target: https://github.com/maxfischer2781/indico_sixpay/tree/master + +Plugin for the Indico event management system to use EPayment via SIX Payment services. + +Quick Guide +----------- + +To enable the plugin, it must be installed for the python version running ``indico``. + +.. code:: bash + + python -m pip install indico_sixpay + +Once installed, it can be enabled in the administrator and event settings. +Configuration uses the same options for global defaults and event specific overrides. + +Disclaimer +---------- + +This plugin is in no way endorsed, supported or provided by SIX, Indico or any other service, provider or entity. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +__title__ = 'indico_sixpay' +__summary__ = 'Indico EPayment Plugin for SixPay services' +__url__ = 'https://github.com/maxfischer2781/indico_sixpay' + +__version__ = '2.0.2' +__author__ = 'Max Fischer' +__email__ = 'maxfischer2781@gmail.com' +__copyright__ = '2017 - 2018 %s' % __author__ diff --git a/payment_sixpay/indico_sixpay/__init__.py b/payment_sixpay/indico_sixpay/__init__.py new file mode 100644 index 0000000..ace9ae6 --- /dev/null +++ b/payment_sixpay/indico_sixpay/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +## +## This file is part of the SixPay Indico EPayment Plugin. +## Copyright (C) 2017 - 2018 Max Fischer +## +## This is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 3 of the +## License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SixPay Indico EPayment Plugin;if not, see . diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py new file mode 100644 index 0000000..25c372a --- /dev/null +++ b/payment_sixpay/indico_sixpay/blueprint.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +## +## This file is part of the SixPay Indico EPayment Plugin. +## Copyright (C) 2017 - 2018 Max Fischer +## +## This is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 3 of the +## License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SixPay Indico EPayment Plugin;if not, see . +""" +Definition of callbacks exposed by the Indico server +""" +from __future__ import unicode_literals + +from indico.core.plugins import IndicoPluginBlueprint + +from .request_handlers import SixPayResponseHandler, UserCancelHandler, UserFailureHandler, UserSuccessHandler + + +#: url mount points exposing callbacks +blueprint = IndicoPluginBlueprint( + 'payment_sixpay', __name__, + url_prefix='/event//registrations//payment/response/sixpay' +) + +blueprint.add_url_rule('/failure', 'failure', UserCancelHandler, methods=('GET', 'POST')) +blueprint.add_url_rule('/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST')) +blueprint.add_url_rule('/success', 'success', UserSuccessHandler, methods=('GET', 'POST')) +# Used by SixPay to send an asynchronous notification for the transaction +blueprint.add_url_rule('/ipn', 'notify', SixPayResponseHandler, methods=('Get', 'POST')) diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py new file mode 100644 index 0000000..88f0a8f --- /dev/null +++ b/payment_sixpay/indico_sixpay/plugin.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +## +## This file is part of the SixPay Indico EPayment Plugin. +## Copyright (C) 2017 - 2018 Max Fischer +## +## This is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 3 of the +## License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SixPay Indico EPayment Plugin;if not, see . +""" +Core of the SixPay plugin + +The entry point for indico is the :py:class:`~.SixpayPaymentPlugin`. +It handles configuration via the settings forms, initiates payments +and provides callbacks for finished payments via its blueprint. +""" +from __future__ import unicode_literals, absolute_import +import urlparse + +import requests +from werkzeug.exceptions import NotImplemented as HTTPNotImplemented, InternalServerError as HTTPInternalServerError + +from wtforms.fields.core import StringField +from wtforms.fields.html5 import URLField +from wtforms.validators import DataRequired, Optional, Regexp, Length, Email, ValidationError + +from indico.core.plugins import IndicoPlugin, url_for_plugin +from indico.modules.events.payment import \ + PaymentEventSettingsFormBase, PaymentPluginMixin, PaymentPluginSettingsFormBase + +from .utility import gettext, to_small_currency +# blueprint mounts the request handlers onto URLs +from .blueprint import blueprint + + +# Dear Future Maintainer, +# +# while an improvement over Indico 1.2, the Indico 2.0 plugin/core +# facilities are rather lacking in documentation. I have added +# some notes for each **base**type on how they are integrated with +# the rest of Indico. Be aware that this is reconstructed from +# implementations, not from official API docs. +# +# Regards, +# Past Maintainer + + +# PaymentPluginSettingsFormBase from indico.modules.events.payment +# - A codified Form for users to fill in. The *class attributes* define +# which fields exist, their shape, description, etc. +# - Each field is a type from wtforms.fields.core.Field. You probably want to have: +# - label: Name of the field, an internationalised identifier +# - validators: Input validation, see wtforms.validators +# - description: help text of the field, an internationalised text + +class FormatField(object): + """ + Validator for format fields, i.e. strings with ``{key}`` placeholders + + :param max_length: optional maximum length, checked on a test formatting + :type max_length: int + :param field_map: keyword arguments to use for test formatting + + On validation, a test mapping is applied to the field. This ensures the + field has a valid ``str.format`` format, and does not use illegal keys + (as determined by ``default_field_map`` and ``field_map``). + The ``max_length`` is validated against the test-formatted field, which + is an estimate for an average sized input. + """ + #: default placeholders to test length after formatting + default_field_map = { + 'user_id': 1234, + 'user_name': 'Jane Whiteacre', + 'event_id': 123, + 'event_title': 'Placeholder: The Event', + 'eventuser_id': 'e123u1234', + 'registration_title': 'EarlyBird Registration' + } + + def __init__(self, max_length=float('inf'), field_map=None): + self.max_length = max_length + self.field_map = self.default_field_map.copy() + if field_map is not None: + self.field_map.update(field_map) + + def __call__(self, form, field): + if not field.data: + return True + try: + test_format = field.data.format(**self.field_map) + except KeyError as err: + raise ValidationError('Invalid format string key: {}'.format(err)) + except ValueError as err: + raise ValidationError('Malformed format string: {}'.format(err)) + if len(test_format) > self.max_length: + raise ValidationError( + 'Too long format string: shortest replacement with {0}, expected {1}'.format( + len(test_format), self.max_length + ) + ) + else: + return True + + +class PluginSettingsForm(PaymentPluginSettingsFormBase): + """Configuration form for the Plugin across all events""" + url = URLField( + gettext('SixPay Saferpay URL'), + [DataRequired()], + description=gettext('URL to contact the Six Payment Service'), + ) + account_id = StringField( + label='Account ID', + # can be set EITHER or BOTH globally and per event + validators=[Optional(), Regexp(r'[0-9-]{0,15}', message='Field must contain up to 15 digits or "-".')], + description=gettext('Default ID of your Saferpay account, such as "401860-17795278".') + ) + order_description = StringField( + label=gettext('Order Description'), + validators=[DataRequired(), FormatField(max_length=80)], + description=gettext('The description of each order in a human readable way.' + 'This description is presented to the registrant during the transaction with SixPay.') + ) + order_identifier = StringField( + label=gettext('Order Identifier'), + validators=[DataRequired(), FormatField(max_length=80)], + description=gettext('The identifier of each order for further processing.') + ) + notification_mail = StringField( + label=gettext('Notification Email'), + validators=[Optional(), Email(), Length(0, 50)], + description=gettext('Mail address to receive notifications of transactions.' + 'This is independent of Indico\'s own payment notifications.') + ) + + +class EventSettingsForm(PaymentEventSettingsFormBase): + """Configuration form for the Plugin for a specific event""" + # every setting may be overwritten for each event + url = PluginSettingsForm.url + account_id = PluginSettingsForm.account_id + order_description = PluginSettingsForm.order_description + order_identifier = PluginSettingsForm.order_identifier + notification_mail = PluginSettingsForm.notification_mail + + +# PaymentPluginMixin, IndicoPlugin +# This is basically a registry of setting fields, logos and other rendering stuff +# All the business logic is in :py:func:`adjust_payment_form_data` +class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): + """ + SixPay Saferpay + + Provides an EPayment method using the SixPay Saferpay API. + """ + configurable = True + #: form for default configuration across events + settings_form = PluginSettingsForm + #: form for configuration for specific events + event_settings_form = EventSettingsForm + #: global default settings - should be a reasonable default + default_settings = { + 'method_name': 'SixPay', + 'url': 'https://www.saferpay.com/hosting/', + 'account_id': None, + 'order_description': '{event_title}, {registration_title}, {user_name}', + 'order_identifier': '{eventuser_id}', + 'notification_mail': None + } + #: per event default settings - use the global settings + default_event_settings = { + 'enabled': False, + 'method_name': None, + 'url': None, + 'account_id': None, + 'order_description': None, + 'order_identifier': None, + 'notification_mail': None, + } + + def get_blueprints(self): + """Blueprint for URL endpoints with callbacks""" + return blueprint + + # Dear Future Maintainer, + # - business logic is here! + # - see PaymentPluginMixin.render_payment_form for what `data` provides + # - What happens here + # - We already send all payment details to SixPay to get a *signed* request url for this transaction + # - We have added `success`, `cancel` and `failure` for *sixpay* to redirect the user back to us AFTER his request + # - We have added `notify` for *sixpay* to inform us asynchronously about the result + # - We put the transaction URL we got into `data` for the *user* to perform his request securely + # - Return uses `indico_sixpay/templates/event_payment_form.html`, presenting a trigger button to the user + def adjust_payment_form_data(self, data): + """Prepare the payment form shown to registrants""" + registration = data['registration'] + # indico does not seem to provide stacking of settings + # we merge event on top of global settings, but remove placeholder defaults + event_settings, global_settings = data['event_settings'], data['settings'] + plugin_settings = { + key: event_settings[key] if event_settings.get(key) is not None else global_settings[key] + for key in + (set(event_settings) | set(global_settings)) + } + # parameters of the transaction - amount, currency, ... + transaction = self._get_transaction_parameters(data) + # callbacks of the transaction - where to announce success, failure, ... + # where to redirect the user + transaction['SUCCESSLINK'] = url_for_plugin('payment_sixpay.success', registration.locator.uuid, _external=True) + transaction['BACKLINK'] = url_for_plugin('payment_sixpay.cancel', registration.locator.uuid, _external=True) + transaction['FAILLINK'] = url_for_plugin('payment_sixpay.failure', registration.locator.uuid, _external=True) + # where to asynchronously call back from SixPay + transaction['NOTIFYURL'] = url_for_plugin('payment_sixpay.notify', registration.locator.uuid, _external=True) + data['payment_url'] = self._get_payment_url(sixpay_url=plugin_settings.get('url'), transaction_data=transaction) + return data + + @staticmethod + def get_field_format_map(registration): + """Generates dict which provides registration information for format fields""" + return { + 'user_id': registration.user_id, + 'user_name': registration.full_name, + 'user_firstname': registration.first_name, + 'user_lastname': registration.last_name, + 'event_id': registration.event_id, + 'event_title': registration.event.title, + 'eventuser_id': 'e{0}u{1}'.format(registration.event_id, registration.user_id), + 'registration_title': registration.registration_form.title + } + + def _get_transaction_parameters(self, payment_data): + """Parameters for formulating a transaction request *without* any business logic hooks""" + plugin_settings = payment_data['event_settings'] + format_map = self.get_field_format_map(payment_data['registration']) + for format_field in 'order_description', 'order_identifier': + try: + if not plugin_settings.has_key(format_field): + raise KeyError + payment_data[format_field] = plugin_settings.get(format_field).format(**format_map) + except ValueError: + message = "Invalid format field placeholder for {0}, please contact the event organisers!" + raise HTTPNotImplemented(( + gettext(message) + '\n\n[' + message + ']' + ).format(self.name) + ) + except KeyError: + message = "Unknown format field placeholder '{0}' for {1}, please contact the event organisers!" + raise HTTPNotImplemented(( + gettext(message) + '\n\n[' + message + ']' + ).format(format_field, self.name) + ) + # see the SixPay Manual on what these things mean + transaction_parameters = { + 'ACCOUNTID': str(plugin_settings.get('account_id')), + # indico handles price as largest currency, but six expects smallest + # e.g. EUR: indico uses 100.2 Euro, but six expects 10020 Cent + 'AMOUNT': '{:.0f}'.format(to_small_currency(payment_data['amount'], payment_data['currency'])), + 'CURRENCY': payment_data['currency'], + 'DESCRIPTION': payment_data['order_description'][:50], + 'ORDERID': payment_data['order_identifier'][:80], + 'SHOWLANGUAGES': 'yes', + } + if plugin_settings.get('notification_mail'): + transaction_parameters['NOTIFYADDRESS'] = plugin_settings.get('notification_mail') + return transaction_parameters + + def _get_payment_url(self, sixpay_url, transaction_data): + """Send transaction data to SixPay to get a signed URL for the user request""" + endpoint = urlparse.urljoin(sixpay_url, 'CreatePayInit.asp') + url_request = requests.post(endpoint, data=transaction_data) + # raise any HTTP errors + url_request.raise_for_status() + if url_request.text.startswith('ERROR'): + raise HTTPInternalServerError('Failed request to SixPay service: %s' % url_request.text) + return url_request.text diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py new file mode 100644 index 0000000..5b701ff --- /dev/null +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +## +## This file is part of the SixPay Indico EPayment Plugin. +## Copyright (C) 2017 - 2018 Max Fischer +## +## This is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 3 of the +## License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SixPay Indico EPayment Plugin;if not, see . +""" +Callbacks for asynchronous replies by the SixPay service and to redirect the user +""" +from __future__ import unicode_literals +import urlparse +from xml.dom.minidom import parseString + +import requests +from flask import flash, redirect, request +from flask_pluginengine import current_plugin +from werkzeug.exceptions import BadRequest + +from indico.modules.events.payment.models.transactions import TransactionAction +from indico.modules.events.payment.notifications import notify_amount_inconsistency +from indico.modules.events.payment.util import register_transaction +from indico.modules.events.registration.models.registrations import Registration +from indico.web.flask.util import url_for +from indico.web.rh import RH + +from .utility import gettext, to_large_currency, to_small_currency + +# RH from indico.web.rh +# - the logic to execute when SixPay/Users are redirected *after* a transaction +# - see blueprint.py for how RH's are mounted! + + +class BaseRequestHandler(RH): + """ + Request Handler for asynchronous callbacks from SixPay + + These handlers are used either by + + - the user, when he is redirected from SixPay back to Indico + - SixPay, when it sends back the result of a transaction + """ + CSRF_ENABLED = False + + def _process_args(self): + self.token = request.args['token'] + self.registration = Registration.find_first(uuid=self.token) + if not self.registration: + raise BadRequest + + +class TransactionFailure(Exception): + """ + A Transaction with SixPay failed + + :param step: name of the step at which the transaction failed + :type step: basestring + :param details: verbose description of what went wrong + :type step: basestring + """ + def __init__(self, step, details=None): + self.step = step + self.details = details + + +class SixPayResponseHandler(BaseRequestHandler): + """Handler for notification from SixPay service""" + def __init__(self): + super(SixPayResponseHandler, self).__init__() + # registration context is not initialised before `self._process_args`... + self.sixpay_url = None # type: str + + def _process_args(self): + super(SixPayResponseHandler, self)._process_args() + # prefer event supplied sixpay url over global sixpay url + self.sixpay_url = current_plugin.event_settings.get(self.registration.registration_form.event, 'url') \ + or current_plugin.settings.get('url') + + def _process(self): + """process the reply from SixPay about the transaction""" + try: + self._process_confirmation() + except TransactionFailure as err: + current_plugin.logger.warning("SixPay transaction failed during %s: %s" % (err.step, err.details)) + + def _process_confirmation(self): + """Process the confirmation response inside indico""" + # DATA: '' + transaction_xml = request.args['DATA'] + transaction_signature = request.args['SIGNATURE'] + transaction_data = self._parse_transaction_xml(transaction_xml) + # verify the signature of SixPay for the transaction + # if this matches, the user completed the transaction as requested by Indico + try: + self._verify_signature(transaction_xml, transaction_signature, transaction_data['ID']) + if self._is_duplicate_transaction(transaction_data=transaction_data): + # we have already handled the transaction + return True + if self._confirm_transaction(transaction_data): + self._verify_amount(transaction_data) + self._register_transaction(transaction_data) + except TransactionFailure as err: + current_plugin.logger.warning("SixPay transaction failed during %s: %s" % (err.step, err.details)) + raise + return True + + def _perform_request(self, task, endpoint, **data): + """ + Helper for performing a request against SixPay + + :param task: description of the request, used for error handling + :type task: basestring + :param endpoint: the URL endpoint *relative* to the SixPay base URL + :type endpoint: basestring + :param **data: data passed during the request + + This will automatically raise any HTTP errors encountered during the request. + If the request itself fails, a :py:exc:`~.TransactionFailure` is raised for ``task``. + """ + request_url = urlparse.urljoin(self.sixpay_url, endpoint) + response = requests.post(request_url, data) + response.raise_for_status() + if response.text.startswith('ERROR'): + raise TransactionFailure(step=task, details=response.text) + return response.text + + @staticmethod + def _parse_transaction_xml(transaction_xml): + """Parse the ``transaction_xml`` to a mapping""" + mdom = parseString(transaction_xml) + attributes = mdom.documentElement.attributes + idp_data = { + attributes.item(idx).name: attributes.item(idx).value + for idx in range(attributes.length) + } + return idp_data + + def _verify_signature(self, transaction_xml, transaction_signature, transaction_id): + """Verify the transaction data and signature with SixPay""" + verification_response = self._perform_request( + 'verification', 'VerifyPayConfirm.asp', + DATA=transaction_xml, SIGNATURE=transaction_signature + ) + if verification_response.startswith('OK'): + # text = 'OK:ID=56a77rg243asfhmkq3r&TOKEN=%3e235462FA23C4FE4AF65' + content = verification_response.split(':', 1)[1] + confirmation = dict(key_value.split('=') for key_value in content.split('&')) + if not confirmation['ID'] == transaction_id: + raise TransactionFailure(step='verification', details='mismatched transaction ID') + return True + raise RuntimeError("Expected reply 'OK:ID=...&TOKEN=...', got %r" % verification_response.text) + + def _is_duplicate_transaction(self, transaction_data): + """Check if this transaction has already been recorded""" + prev_transaction = self.registration.transaction + if not prev_transaction or prev_transaction.provider != 'sixpay': + return False + return all( + prev_transaction.data.get(key) == transaction_data.get(key) + for key in ('ORDERID', 'CURRENCY', 'AMOUNT', 'ACCOUNTID') + ) + + def _verify_amount(self, transaction_data): + """Verify the amount and currency of the payment; sends an email but still registers incorrect payments""" + expected_amount = float(self.registration.price) + expected_currency = self.registration.currency + amount = float(transaction_data['AMOUNT']) + currency = transaction_data['CURRENCY'] + if to_small_currency(expected_amount, expected_currency) == amount and expected_currency == currency: + return True + current_plugin.logger.warning( + "Payment doesn't match events fee: %s %s != %s %s", + amount, currency, to_small_currency(expected_amount, expected_currency), expected_currency + ) + notify_amount_inconsistency(self.registration, to_large_currency(amount, currency), currency) + return False + + def _confirm_transaction(self, transaction_data): + """Confirm to SixPay that the transaction is accepted""" + completion_data = {'ACCOUNTID': transaction_data['ACCOUNTID'], 'ID': transaction_data['ID']} + if 'test.saferpay.com' in self.sixpay_url: + # password: see "Saferpay Payment Page" specification, v5.1, section 4.6 + completion_data['spPassword'] = '8e7Yn5yk' + completion_response = self._perform_request('confirmation', 'PayCompleteV2.asp', **completion_data) + assert completion_response.startswith('OK') + return True + + def _register_transaction(self, transaction_data): + """Register the transaction persistently within Indico""" + register_transaction( + registration=self.registration, + # SixPay uses SMALLEST currency, Indico expects LARGEST currency + amount=to_large_currency(float(transaction_data['AMOUNT']), transaction_data['CURRENCY']), + currency=transaction_data['CURRENCY'], + action=TransactionAction.complete, + provider='sixpay', + data=transaction_data, + ) + + +class UserCancelHandler(BaseRequestHandler): + """User Message on cancelled payment""" + def _process(self): + flash(gettext('You cancelled the payment.'), 'info') + return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + + +class UserFailureHandler(BaseRequestHandler): + """User Message on failed payment""" + def _process(self): + flash(gettext('Your payment has failed.'), 'info') + return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + + +class UserSuccessHandler(SixPayResponseHandler): + """User Message on successful payment""" + def _process(self): + try: + self._process_confirmation() + except TransactionFailure as err: + current_plugin.logger.warning("SixPay transaction failed during %s: %s" % (err.step, err.details)) + flash(gettext('Your payment could not be confirmed. Please contact an organizer.'), 'info') + else: + flash(gettext('Your payment has been confirmed.'), 'success') + return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) diff --git a/payment_sixpay/indico_sixpay/templates/event_payment_form.html b/payment_sixpay/indico_sixpay/templates/event_payment_form.html new file mode 100644 index 0000000..36f7342 --- /dev/null +++ b/payment_sixpay/indico_sixpay/templates/event_payment_form.html @@ -0,0 +1,16 @@ +Clicking on the {% trans %}Pay Now{% endtrans %} button you will get redirected to the SixPay site in order to complete your transaction. + +
+
{% trans %}First name{% endtrans %}
+
{{ registration.first_name }}
+
{% trans %}Last name{% endtrans %}
+
{{ registration.last_name }}
+
{% trans %}Total amount{% endtrans %}
+
{{ format_currency(amount, currency, locale=session.lang) }}
+
+
+
+ +
+
+
diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_sixpay/utility.py new file mode 100644 index 0000000..b89feb6 --- /dev/null +++ b/payment_sixpay/indico_sixpay/utility.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +## +## This file is part of the SixPay Indico EPayment Plugin. +## Copyright (C) 2017 - 2018 Max Fischer +## +## This is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 3 of the +## License, or (at your option) any later version. +## +## This software is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with SixPay Indico EPayment Plugin;if not, see . +from __future__ import unicode_literals, division + +import iso4217 +from werkzeug.exceptions import NotImplemented as HTTPNotImplemented + +from indico.util.i18n import make_bound_gettext + +#: internationalisation/localisation of strings +gettext = make_bound_gettext('payment_sixpay') + + +#: currencies for which the major to minor currency ratio is not a multiple of 10 +NON_DECIMAL_CURRENCY = {'MRU', 'MGA'} + + +def validate_currency(iso_code): + """ + Check whether the currency can be properly handled by this plugin + + :param iso_code: an ISO4217 currency code, e.g. ``"EUR"`` + :type iso_code: basestring + :raises: :py:exc:`~.HTTPNotImplemented` if the currency is not valid + """ + if iso_code in NON_DECIMAL_CURRENCY: + raise HTTPNotImplemented( + gettext("Unsupported currency '{0}' for SixPay. Please contact the organisers").format(iso_code) + ) + try: + iso4217.Currency(iso_code) + except ValueError: + raise HTTPNotImplemented( + gettext("Unknown currency '{0}' for SixPay. Please contact the organisers").format(iso_code) + ) + + +def to_small_currency(large_currency_amount, iso_code): + """ + Convert an amount from large currency to small currency, e.g. 2.3 Euro to 230 Eurocent + + :param large_currency_amount: the amount in large currency, e.g. ``2.3`` + :param iso_code: the ISO currency code, e.g. ``"EUR"`` + :return: the amount in small currency, e.g. ``230`` + """ + validate_currency(iso_code) + exponent = iso4217.Currency(iso_code).exponent + if exponent == 0: + return large_currency_amount + return large_currency_amount * (10 ** exponent) + + +def to_large_currency(small_currency_amount, iso_code): + """Inverse of :py:func:`to_small_currency`""" + validate_currency(iso_code) + exponent = iso4217.Currency(iso_code).exponent + if exponent == 0: + return small_currency_amount + return small_currency_amount / (10 ** exponent) diff --git a/payment_sixpay/setup.py b/payment_sixpay/setup.py new file mode 100644 index 0000000..fc9e88a --- /dev/null +++ b/payment_sixpay/setup.py @@ -0,0 +1,42 @@ +import os +from setuptools import setup, find_packages + +repo_base_dir = os.path.abspath(os.path.dirname(__file__)) +# pull in the packages metadata +package_about = {} +with open(os.path.join(repo_base_dir, "indico_sixpay", "__about__.py")) as about_file: + exec(about_file.read(), package_about) + +setup( + name=package_about['__title__'], + version=package_about['__version__'], + description=package_about['__summary__'], + long_description=package_about['__doc__'].strip(), + author=package_about['__author__'], + author_email=package_about['__email__'], + url=package_about['__url__'], + entry_points={ + 'indico.plugins': { + 'payment_sixpay = indico_sixpay.plugin:SixpayPaymentPlugin' + } + }, + packages=find_packages(), + package_data={'indico_sixpay': ['templates/*.html']}, + install_requires=['requests', 'indico>=2.0', 'iso4217'], + license='GPLv3+', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Plugins', + 'Environment :: Web Environment', + 'Intended Audience :: Science/Research', + 'Intended Audience :: System Administrators', + 'Intended Audience :: Other Audience', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Topic :: Communications :: Conferencing', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + zip_safe=False, + keywords='indico epayment six sixpay plugin', +) From 1953646a23519bc77f9f18c1d3f3031b28747e6e Mon Sep 17 00:00:00 2001 From: Martin Claus Date: Wed, 24 Jul 2019 23:26:09 +0200 Subject: [PATCH 02/14] Update to use new JSON API --- payment_sixpay/indico_sixpay/blueprint.py | 63 +-- payment_sixpay/indico_sixpay/plugin.py | 347 +++++++++++----- .../indico_sixpay/request_handlers.py | 382 +++++++++++------- payment_sixpay/indico_sixpay/utility.py | 115 ++++-- 4 files changed, 616 insertions(+), 291 deletions(-) diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py index 25c372a..8d48893 100644 --- a/payment_sixpay/indico_sixpay/blueprint.py +++ b/payment_sixpay/indico_sixpay/blueprint.py @@ -1,38 +1,51 @@ # -*- coding: utf-8 -*- -## -## This file is part of the SixPay Indico EPayment Plugin. -## Copyright (C) 2017 - 2018 Max Fischer -## -## This is free software; you can redistribute it and/or -## modify it under the terms of the GNU General Public License as -## published by the Free Software Foundation; either version 3 of the -## License, or (at your option) any later version. -## -## This software is distributed in the hope that it will be useful, but -## WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -## General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with SixPay Indico EPayment Plugin;if not, see . -""" -Definition of callbacks exposed by the Indico server -""" +# +# This file is part of the SixPay Indico EPayment Plugin. +# Copyright (C) 2017 - 2018 Max Fischer +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SixPay Indico EPayment Plugin; +# if not, see . +"""Definition of callbacks exposed by the Indico server.""" from __future__ import unicode_literals from indico.core.plugins import IndicoPluginBlueprint -from .request_handlers import SixPayResponseHandler, UserCancelHandler, UserFailureHandler, UserSuccessHandler +from .request_handlers import ( + SixPayResponseHandler, UserCancelHandler, + UserFailureHandler, UserSuccessHandler +) #: url mount points exposing callbacks blueprint = IndicoPluginBlueprint( 'payment_sixpay', __name__, - url_prefix='/event//registrations//payment/response/sixpay' + url_prefix=( + '/event//registrations/' + '/payment/response/sixpay' + ) ) -blueprint.add_url_rule('/failure', 'failure', UserCancelHandler, methods=('GET', 'POST')) -blueprint.add_url_rule('/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST')) -blueprint.add_url_rule('/success', 'success', UserSuccessHandler, methods=('GET', 'POST')) +blueprint.add_url_rule( + '/failure', 'failure', UserCancelHandler, methods=('GET', 'POST') +) +blueprint.add_url_rule( + '/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST') +) +blueprint.add_url_rule( + '/success', 'success', UserSuccessHandler, methods=('GET', 'POST') +) # Used by SixPay to send an asynchronous notification for the transaction -blueprint.add_url_rule('/ipn', 'notify', SixPayResponseHandler, methods=('Get', 'POST')) +blueprint.add_url_rule( + '/ipn', 'notify', SixPayResponseHandler, methods=('Get', 'POST') +) diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py index 88f0a8f..c93099c 100644 --- a/payment_sixpay/indico_sixpay/plugin.py +++ b/payment_sixpay/indico_sixpay/plugin.py @@ -1,22 +1,22 @@ # -*- coding: utf-8 -*- -## -## This file is part of the SixPay Indico EPayment Plugin. -## Copyright (C) 2017 - 2018 Max Fischer -## -## This is free software; you can redistribute it and/or -## modify it under the terms of the GNU General Public License as -## published by the Free Software Foundation; either version 3 of the -## License, or (at your option) any later version. -## -## This software is distributed in the hope that it will be useful, but -## WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -## General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with SixPay Indico EPayment Plugin;if not, see . +# This file is part of the SixPay Indico EPayment Plugin. +# Copyright (C) 2017 - 2018 Max Fischer +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SixPay Indico EPayment Plugin; +# if not, see . """ -Core of the SixPay plugin +Core of the SixPay plugin. The entry point for indico is the :py:class:`~.SixpayPaymentPlugin`. It handles configuration via the settings forms, initiates payments @@ -26,20 +26,37 @@ from __future__ import unicode_literals, absolute_import import urlparse import requests -from werkzeug.exceptions import NotImplemented as HTTPNotImplemented, InternalServerError as HTTPInternalServerError +from werkzeug.exceptions import ( + NotImplemented as HTTPNotImplemented, + InternalServerError as HTTPInternalServerError +) -from wtforms.fields.core import StringField +from wtforms.fields import StringField from wtforms.fields.html5 import URLField -from wtforms.validators import DataRequired, Optional, Regexp, Length, Email, ValidationError +from wtforms.validators import ( + DataRequired, Optional, Regexp, Length, Email, ValidationError +) +from indico.web.forms.fields import IndicoPasswordField from indico.core.plugins import IndicoPlugin, url_for_plugin -from indico.modules.events.payment import \ - PaymentEventSettingsFormBase, PaymentPluginMixin, PaymentPluginSettingsFormBase +from indico.modules.events.payment import ( + PaymentEventSettingsFormBase, + PaymentPluginMixin, + PaymentPluginSettingsFormBase) +from indico.modules.events.payment.models.transactions import ( + PaymentTransaction, + TransactionAction +) -from .utility import gettext, to_small_currency + +from .utility import ( + gettext, to_small_currency, get_request_header, get_terminal_id, + provider +) # blueprint mounts the request handlers onto URLs from .blueprint import blueprint +from .utility import saferpay_json_api_spec, saferpay_pp_init_url # Dear Future Maintainer, # @@ -56,14 +73,13 @@ from .blueprint import blueprint # PaymentPluginSettingsFormBase from indico.modules.events.payment # - A codified Form for users to fill in. The *class attributes* define # which fields exist, their shape, description, etc. -# - Each field is a type from wtforms.fields.core.Field. You probably want to have: +# - Each field is a type from wtforms.fields.core.Field. You probably want: # - label: Name of the field, an internationalised identifier # - validators: Input validation, see wtforms.validators # - description: help text of the field, an internationalised text class FormatField(object): - """ - Validator for format fields, i.e. strings with ``{key}`` placeholders + """Validator for format fields, i.e. strings with ``{key}`` placeholders. :param max_length: optional maximum length, checked on a test formatting :type max_length: int @@ -75,6 +91,7 @@ class FormatField(object): The ``max_length`` is validated against the test-formatted field, which is an estimate for an average sized input. """ + #: default placeholders to test length after formatting default_field_map = { 'user_id': 1234, @@ -86,12 +103,24 @@ class FormatField(object): } def __init__(self, max_length=float('inf'), field_map=None): + """Format field validator, i.e. strings with ``{key}`` placeholders. + + :param max_length: optional maximum length, + checked on a test formatting + :type max_length: int + :param field_map: keyword arguments to use for test formatting + """ self.max_length = max_length self.field_map = self.default_field_map.copy() if field_map is not None: self.field_map.update(field_map) def __call__(self, form, field): + """Validate format field data. + + Returns true on successful validation, else an ValidationError is + thrown. + """ if not field.data: return True try: @@ -102,7 +131,9 @@ class FormatField(object): raise ValidationError('Malformed format string: {}'.format(err)) if len(test_format) > self.max_length: raise ValidationError( - 'Too long format string: shortest replacement with {0}, expected {1}'.format( + 'Too long format string:' + ' shortest replacement with {0}, expected {1}' + .format( len(test_format), self.max_length ) ) @@ -111,41 +142,72 @@ class FormatField(object): class PluginSettingsForm(PaymentPluginSettingsFormBase): - """Configuration form for the Plugin across all events""" + """Configuration form for the Plugin across all events.""" + url = URLField( - gettext('SixPay Saferpay URL'), - [DataRequired()], + label=gettext('SixPay Saferpay URL'), + validators=[DataRequired()], description=gettext('URL to contact the Six Payment Service'), ) + username = StringField( + label=gettext('Username'), + validators=[DataRequired()], + description=gettext('SaferPay JSON API User name.') + ) + password = IndicoPasswordField( + label=gettext('Password'), + validators=[DataRequired()], + description=gettext('SaferPay JSON API User password.'), + toggle=True, + ) account_id = StringField( label='Account ID', # can be set EITHER or BOTH globally and per event - validators=[Optional(), Regexp(r'[0-9-]{0,15}', message='Field must contain up to 15 digits or "-".')], - description=gettext('Default ID of your Saferpay account, such as "401860-17795278".') + validators=[ + Optional(), + Regexp( + r'[0-9-]{0,15}', + message='Field must contain up to 15 digits or "-".' + ) + ], + description=gettext( + 'Default ID of your Saferpay account,' + ' such as "401860-17795278".' + ) ) order_description = StringField( label=gettext('Order Description'), validators=[DataRequired(), FormatField(max_length=80)], - description=gettext('The description of each order in a human readable way.' - 'This description is presented to the registrant during the transaction with SixPay.') + description=gettext( + 'The description of each order in a human readable way. ' + 'This description is presented to the registrant during the ' + 'transaction with SixPay.' + ) ) order_identifier = StringField( label=gettext('Order Identifier'), validators=[DataRequired(), FormatField(max_length=80)], - description=gettext('The identifier of each order for further processing.') + description=gettext( + 'The identifier of each order for further processing.' + ) ) notification_mail = StringField( label=gettext('Notification Email'), validators=[Optional(), Email(), Length(0, 50)], - description=gettext('Mail address to receive notifications of transactions.' - 'This is independent of Indico\'s own payment notifications.') + description=gettext( + 'Mail address to receive notifications of transactions.' + 'This is independent of Indico\'s own payment notifications.' + ) ) class EventSettingsForm(PaymentEventSettingsFormBase): - """Configuration form for the Plugin for a specific event""" + """Configuration form for the Plugin for a specific event.""" + # every setting may be overwritten for each event - url = PluginSettingsForm.url + #url = PluginSettingsForm.url + #username = PluginSettingsForm.username + #password = PluginSettingsForm.password account_id = PluginSettingsForm.account_id order_description = PluginSettingsForm.order_description order_identifier = PluginSettingsForm.order_identifier @@ -153,14 +215,15 @@ class EventSettingsForm(PaymentEventSettingsFormBase): # PaymentPluginMixin, IndicoPlugin -# This is basically a registry of setting fields, logos and other rendering stuff +# This is basically a registry of setting fields, +# logos and other rendering stuff. # All the business logic is in :py:func:`adjust_payment_form_data` class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): - """ - SixPay Saferpay + """SixPay Saferpay plugin. Provides an EPayment method using the SixPay Saferpay API. """ + configurable = True #: form for default configuration across events settings_form = PluginSettingsForm @@ -169,9 +232,12 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): #: global default settings - should be a reasonable default default_settings = { 'method_name': 'SixPay', - 'url': 'https://www.saferpay.com/hosting/', + 'url': 'https://www.saferpay.com/api/', + 'username': None, + 'password': None, 'account_id': None, - 'order_description': '{event_title}, {registration_title}, {user_name}', + 'order_description': + '{event_title}, {registration_title}, {user_name}', 'order_identifier': '{eventuser_id}', 'notification_mail': None } @@ -179,7 +245,9 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): default_event_settings = { 'enabled': False, 'method_name': None, - 'url': None, + # 'url': None, + # 'username': None, + # 'password': None, 'account_id': None, 'order_description': None, 'order_identifier': None, @@ -187,44 +255,61 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): } def get_blueprints(self): - """Blueprint for URL endpoints with callbacks""" + """Blueprint for URL endpoints with callbacks.""" return blueprint # Dear Future Maintainer, # - business logic is here! # - see PaymentPluginMixin.render_payment_form for what `data` provides # - What happens here - # - We already send all payment details to SixPay to get a *signed* request url for this transaction - # - We have added `success`, `cancel` and `failure` for *sixpay* to redirect the user back to us AFTER his request - # - We have added `notify` for *sixpay* to inform us asynchronously about the result - # - We put the transaction URL we got into `data` for the *user* to perform his request securely - # - Return uses `indico_sixpay/templates/event_payment_form.html`, presenting a trigger button to the user + # - We add `success`, `cancel` and `failure` for *sixpay* to redirect the + # user back to us AFTER his request + # - We add `notify` for *sixpay* to inform us asynchronously about + # the result + # - We send a request to initialize the pyment page to SixPay to get a + # request url for this transaction + # - We put the payment page URL and token we got into `data` + # - Return uses `indico_sixpay/templates/event_payment_form.html`, + # presenting a trigger button to the user def adjust_payment_form_data(self, data): - """Prepare the payment form shown to registrants""" - registration = data['registration'] + """Prepare the payment form shown to registrants.""" # indico does not seem to provide stacking of settings - # we merge event on top of global settings, but remove placeholder defaults - event_settings, global_settings = data['event_settings'], data['settings'] + # we merge event on top of global settings, but remove defaults + event_settings = data['event_settings'] + global_settings = data['settings'] plugin_settings = { - key: event_settings[key] if event_settings.get(key) is not None else global_settings[key] - for key in - (set(event_settings) | set(global_settings)) + key: event_settings[key] + if event_settings.get(key) is not None + else global_settings[key] + for key in (set(event_settings) | set(global_settings)) } # parameters of the transaction - amount, currency, ... - transaction = self._get_transaction_parameters(data) - # callbacks of the transaction - where to announce success, failure, ... - # where to redirect the user - transaction['SUCCESSLINK'] = url_for_plugin('payment_sixpay.success', registration.locator.uuid, _external=True) - transaction['BACKLINK'] = url_for_plugin('payment_sixpay.cancel', registration.locator.uuid, _external=True) - transaction['FAILLINK'] = url_for_plugin('payment_sixpay.failure', registration.locator.uuid, _external=True) - # where to asynchronously call back from SixPay - transaction['NOTIFYURL'] = url_for_plugin('payment_sixpay.notify', registration.locator.uuid, _external=True) - data['payment_url'] = self._get_payment_url(sixpay_url=plugin_settings.get('url'), transaction_data=transaction) + transaction = self._get_transaction_parameters(data, plugin_settings) + init_response = self._init_payment_page( + sixpay_url=plugin_settings['url'], + transaction_data=transaction, + credentials=( + plugin_settings['username'], plugin_settings['password']) + ) + data['payment_url'] = init_response['RedirectUrl'] + + # create pending transaction and store Saferpay transaction token + if not PaymentTransaction.create_next( + registration=data['registration'], + amount=data['amount'], + currency=data['currency'], + action=TransactionAction.pending, + provider=provider, + data={'Init_PP_response': init_response} + ): + data['registration'].transaction.data = { + 'Init_PP_response': init_response + } return data @staticmethod def get_field_format_map(registration): - """Generates dict which provides registration information for format fields""" + """Generate dict which provides registration information.""" return { 'user_id': registration.user_id, 'user_name': registration.full_name, @@ -232,52 +317,116 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): 'user_lastname': registration.last_name, 'event_id': registration.event_id, 'event_title': registration.event.title, - 'eventuser_id': 'e{0}u{1}'.format(registration.event_id, registration.user_id), + 'eventuser_id': + 'e{0}u{1}'.format(registration.event_id, registration.user_id), 'registration_title': registration.registration_form.title } - def _get_transaction_parameters(self, payment_data): - """Parameters for formulating a transaction request *without* any business logic hooks""" - plugin_settings = payment_data['event_settings'] - format_map = self.get_field_format_map(payment_data['registration']) + def _get_transaction_parameters(self, payment_data, plugin_settings): + """Parameters for formulating a transaction request.""" + registration = payment_data['registration'] + format_map = self.get_field_format_map(registration) for format_field in 'order_description', 'order_identifier': try: - if not plugin_settings.has_key(format_field): - raise KeyError - payment_data[format_field] = plugin_settings.get(format_field).format(**format_map) + payment_data[format_field] = ( + plugin_settings[format_field].format(**format_map) + ) except ValueError: - message = "Invalid format field placeholder for {0}, please contact the event organisers!" - raise HTTPNotImplemented(( - gettext(message) + '\n\n[' + message + ']' - ).format(self.name) + message = ( + "Invalid format field placeholder for {0}," + " please contact the event organisers!" + ) + raise HTTPNotImplemented( + (gettext(message) + '\n\n[' + message + ']') + .format(self.name) ) except KeyError: - message = "Unknown format field placeholder '{0}' for {1}, please contact the event organisers!" + message = ( + 'Unknown format field placeholder "{0}" for {1},' + ' please contact the event organisers!' + ) raise HTTPNotImplemented(( gettext(message) + '\n\n[' + message + ']' ).format(format_field, self.name) ) - # see the SixPay Manual on what these things mean + + # see the SixPay Manual + # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize + # on what these things mean transaction_parameters = { - 'ACCOUNTID': str(plugin_settings.get('account_id')), - # indico handles price as largest currency, but six expects smallest - # e.g. EUR: indico uses 100.2 Euro, but six expects 10020 Cent - 'AMOUNT': '{:.0f}'.format(to_small_currency(payment_data['amount'], payment_data['currency'])), - 'CURRENCY': payment_data['currency'], - 'DESCRIPTION': payment_data['order_description'][:50], - 'ORDERID': payment_data['order_identifier'][:80], - 'SHOWLANGUAGES': 'yes', + 'RequestHeader': get_request_header( + saferpay_json_api_spec, plugin_settings['account_id'] + ), + 'TerminalId': str( + get_terminal_id(plugin_settings['account_id']) + ), + 'Payment': { + 'Amount': { + # indico handles price as largest currency, but six expects + # smallest. E.g. EUR: indico uses 100.2 Euro, but six + # expects 10020 Cent + 'Value': '{:d}'.format( + to_small_currency( + payment_data['amount'], + payment_data['currency'] + ) + ), + 'CurrencyCode': payment_data['currency'], + }, + 'OrderId': payment_data['order_identifier'][:80], + 'DESCRIPTION': payment_data['order_description'][:1000], + }, + # callbacks of the transaction - where to announce success, ... + # where to redirect the user + 'ReturnUrls': { + 'Success': url_for_plugin( + 'payment_sixpay.success', + registration.locator.uuid, + _external=True + ), + 'Fail': url_for_plugin( + 'payment_sixpay.failure', + registration.locator.uuid, + _external=True + ), + 'Abort': url_for_plugin( + 'payment_sixpay.cancel', + registration.locator.uuid, + _external=True + ) + }, + 'Notification': { + # where to asynchronously call back from SixPay + 'NotifyUrl': url_for_plugin( + 'payment_sixpay.notify', + registration.locator.uuid, + _external=True + ) + } } - if plugin_settings.get('notification_mail'): - transaction_parameters['NOTIFYADDRESS'] = plugin_settings.get('notification_mail') + if 'notification_mail' in plugin_settings: + transaction_parameters['Notification']['MerchantEmails'] = ( + plugin_settings['notification_mail'] + ) return transaction_parameters - def _get_payment_url(self, sixpay_url, transaction_data): - """Send transaction data to SixPay to get a signed URL for the user request""" - endpoint = urlparse.urljoin(sixpay_url, 'CreatePayInit.asp') - url_request = requests.post(endpoint, data=transaction_data) + def _init_payment_page(self, sixpay_url, transaction_data, credentials): + """Initialize payment page.""" + endpoint = urlparse.urljoin(sixpay_url, saferpay_pp_init_url) + url_request = requests.post( + endpoint, + json=transaction_data, + auth=credentials + ) # raise any HTTP errors url_request.raise_for_status() - if url_request.text.startswith('ERROR'): - raise HTTPInternalServerError('Failed request to SixPay service: %s' % url_request.text) - return url_request.text + response = url_request.json() + if 'ErrorName' in response: + if 'ErrorDetail' not in response: + response['ErrorDetail'] = '' + raise HTTPInternalServerError( + 'Failed request to SixPay service:' + ' {ErrorMessage}. {ErrorDetail}' + .format(response) + ) + return response diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py index 5b701ff..3b7d4a4 100644 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -1,26 +1,24 @@ # -*- coding: utf-8 -*- -## -## This file is part of the SixPay Indico EPayment Plugin. -## Copyright (C) 2017 - 2018 Max Fischer -## -## This is free software; you can redistribute it and/or -## modify it under the terms of the GNU General Public License as -## published by the Free Software Foundation; either version 3 of the -## License, or (at your option) any later version. -## -## This software is distributed in the hope that it will be useful, but -## WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -## General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with SixPay Indico EPayment Plugin;if not, see . -""" -Callbacks for asynchronous replies by the SixPay service and to redirect the user -""" +# +# This file is part of the SixPay Indico EPayment Plugin. +# Copyright (C) 2017 - 2018 Max Fischer +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SixPay Indico EPayment Plugin; +# if not, see . +"""Callbacks for asynchronous replies by Saferpay and to redirect the user.""" from __future__ import unicode_literals import urlparse -from xml.dom.minidom import parseString import requests from flask import flash, redirect, request @@ -28,13 +26,24 @@ from flask_pluginengine import current_plugin from werkzeug.exceptions import BadRequest from indico.modules.events.payment.models.transactions import TransactionAction -from indico.modules.events.payment.notifications import notify_amount_inconsistency -from indico.modules.events.payment.util import register_transaction -from indico.modules.events.registration.models.registrations import Registration +from indico.modules.events.payment.notifications \ + import notify_amount_inconsistency +from indico.modules.events.registration.models.registrations \ + import Registration from indico.web.flask.util import url_for from indico.web.rh import RH -from .utility import gettext, to_large_currency, to_small_currency +from .utility import ( + gettext, to_large_currency, to_small_currency, get_request_header, + get_setting +) +from .utility import ( + saferpay_pp_assert_url, + saferpay_pp_capture_url, + saferpay_json_api_spec, + saferpay_pp_cancel_url, + provider +) # RH from indico.web.rh # - the logic to execute when SixPay/Users are redirected *after* a transaction @@ -42,99 +51,95 @@ from .utility import gettext, to_large_currency, to_small_currency class BaseRequestHandler(RH): - """ - Request Handler for asynchronous callbacks from SixPay + """Request Handler for asynchronous callbacks from SixPay. These handlers are used either by - the user, when he is redirected from SixPay back to Indico - SixPay, when it sends back the result of a transaction """ + CSRF_ENABLED = False def _process_args(self): - self.token = request.args['token'] - self.registration = Registration.find_first(uuid=self.token) + self.registration = Registration.find_first(uuid=request.args['token']) if not self.registration: raise BadRequest + self.token = ( + self.registration.transaction.data['Init_PP_response']['Token'] + ) + + def _get_setting(self, setting): + return get_setting(setting, self.registration.registration_form.event) class TransactionFailure(Exception): - """ - A Transaction with SixPay failed + """A Transaction with SixPay failed. :param step: name of the step at which the transaction failed :type step: basestring :param details: verbose description of what went wrong :type step: basestring """ + def __init__(self, step, details=None): + """Initialize request handler.""" self.step = step self.details = details class SixPayResponseHandler(BaseRequestHandler): - """Handler for notification from SixPay service""" + """Handler for notification from SixPay service.""" + def __init__(self): + """Initialize request handler.""" super(SixPayResponseHandler, self).__init__() - # registration context is not initialised before `self._process_args`... + # registration context is not initialised before `self._process_args` self.sixpay_url = None # type: str def _process_args(self): super(SixPayResponseHandler, self)._process_args() - # prefer event supplied sixpay url over global sixpay url - self.sixpay_url = current_plugin.event_settings.get(self.registration.registration_form.event, 'url') \ - or current_plugin.settings.get('url') + self.sixpay_url = get_setting('url') def _process(self): - """process the reply from SixPay about the transaction""" + """Process the reply from SixPay about the transaction.""" try: self._process_confirmation() except TransactionFailure as err: - current_plugin.logger.warning("SixPay transaction failed during %s: %s" % (err.step, err.details)) + current_plugin.logger.warning( + "SixPay transaction failed during %s: %s" + % (err.step, err.details) + ) def _process_confirmation(self): - """Process the confirmation response inside indico""" - # DATA: '' - transaction_xml = request.args['DATA'] - transaction_signature = request.args['SIGNATURE'] - transaction_data = self._parse_transaction_xml(transaction_xml) - # verify the signature of SixPay for the transaction - # if this matches, the user completed the transaction as requested by Indico + """Process the confirmation response inside indico.""" + transaction_data = request.json + # transaction_signature = request.args['SIGNATURE'] + # transaction_data = self._parse_transaction_xml(transaction_xml) + # assert transaction status from SixPay try: - self._verify_signature(transaction_xml, transaction_signature, transaction_data['ID']) - if self._is_duplicate_transaction(transaction_data=transaction_data): + assert_response = self._assert_payment(transaction_data) + if self._is_duplicate_transaction(assert_response): # we have already handled the transaction return True - if self._confirm_transaction(transaction_data): - self._verify_amount(transaction_data) - self._register_transaction(transaction_data) + if ( + self._is_authorized(assert_response) + and not self._is_captured(assert_response) + ): + capture_response = self._capture_transaction(assert_response) + assert_response['CaptureResponse'] = capture_response + self._verify_amount(assert_response) + self._register_payment(assert_response) except TransactionFailure as err: - current_plugin.logger.warning("SixPay transaction failed during %s: %s" % (err.step, err.details)) + current_plugin.logger.warning( + "SixPay transaction failed during %s: %s" + % (err.step, err.details) + ) raise return True - def _perform_request(self, task, endpoint, **data): - """ - Helper for performing a request against SixPay + def _perform_request(self, task, endpoint, data): + """Perform a request against SixPay. :param task: description of the request, used for error handling :type task: basestring @@ -142,112 +147,205 @@ class SixPayResponseHandler(BaseRequestHandler): :type endpoint: basestring :param **data: data passed during the request - This will automatically raise any HTTP errors encountered during the request. - If the request itself fails, a :py:exc:`~.TransactionFailure` is raised for ``task``. + This will automatically raise any HTTP errors encountered during the + request. If the request itself fails, a :py:exc:`~.TransactionFailure` + is raised for ``task``. """ request_url = urlparse.urljoin(self.sixpay_url, endpoint) - response = requests.post(request_url, data) - response.raise_for_status() - if response.text.startswith('ERROR'): - raise TransactionFailure(step=task, details=response.text) - return response.text - - @staticmethod - def _parse_transaction_xml(transaction_xml): - """Parse the ``transaction_xml`` to a mapping""" - mdom = parseString(transaction_xml) - attributes = mdom.documentElement.attributes - idp_data = { - attributes.item(idx).name: attributes.item(idx).value - for idx in range(attributes.length) - } - return idp_data - - def _verify_signature(self, transaction_xml, transaction_signature, transaction_id): - """Verify the transaction data and signature with SixPay""" - verification_response = self._perform_request( - 'verification', 'VerifyPayConfirm.asp', - DATA=transaction_xml, SIGNATURE=transaction_signature + credentials = ( + get_setting('username'), + get_setting('password') ) - if verification_response.startswith('OK'): - # text = 'OK:ID=56a77rg243asfhmkq3r&TOKEN=%3e235462FA23C4FE4AF65' - content = verification_response.split(':', 1)[1] - confirmation = dict(key_value.split('=') for key_value in content.split('&')) - if not confirmation['ID'] == transaction_id: - raise TransactionFailure(step='verification', details='mismatched transaction ID') - return True - raise RuntimeError("Expected reply 'OK:ID=...&TOKEN=...', got %r" % verification_response.text) + response = requests.post( + request_url, json=data, auth=credentials + ) + try: + response.raise_for_status() + except requests.HTTPError: + raise TransactionFailure( + step=task, + details=response.text + ) + return response + + def _assert_payment(self, transaction_data): + """Check the status of the transaction with SixPay. + + Returns transaction assert data. + """ + assert_response = self._perform_request( + 'assert', + saferpay_pp_assert_url, + { + 'RequestHeader': get_request_header( + saferpay_json_api_spec, self._get_setting('account_id') + ), + 'Token': self.token, + } + ) + if assert_response.status_code == requests.codes.ok: + return assert_response.json() def _is_duplicate_transaction(self, transaction_data): - """Check if this transaction has already been recorded""" + """Check if this transaction has already been recorded.""" prev_transaction = self.registration.transaction - if not prev_transaction or prev_transaction.provider != 'sixpay': + if ( + not prev_transaction + or prev_transaction.provider != 'sixpay' + or 'Transaction' not in prev_transaction.data + ): return False - return all( - prev_transaction.data.get(key) == transaction_data.get(key) - for key in ('ORDERID', 'CURRENCY', 'AMOUNT', 'ACCOUNTID') + old = prev_transaction.data.get('Transaction') + new = transaction_data.get('Transaction') + return ( + old['OrderId'] == new['OrderId'] + & old['Type'] == new['Type'] + & old['Id'] == new['Id'] + & old['SixTransactionReference'] == new['SixTransactionReference'] + & old['Amount']['Value'] == new['Amount']['Value'] + & old['Amount']['CurrencyCode'] == new['Amount']['CurrencyCode'] ) - def _verify_amount(self, transaction_data): - """Verify the amount and currency of the payment; sends an email but still registers incorrect payments""" + def _is_authorized(self, assert_data): + """Check if payment is authorized.""" + return assert_data['Transaction']['Status'] == 'AUTHORIZED' + + def _is_captured(self, assert_data): + """Check if payment is captured, i.e. the cash flow is triggered.""" + return assert_data['Transaction']['Status'] == 'CAPTURED' + + def _verify_amount(self, assert_data): + """Verify the amount and currency of the payment. + + Sends an email but still registers incorrect payments. + """ expected_amount = float(self.registration.price) expected_currency = self.registration.currency - amount = float(transaction_data['AMOUNT']) - currency = transaction_data['CURRENCY'] - if to_small_currency(expected_amount, expected_currency) == amount and expected_currency == currency: + amount = float(assert_data['Transaction']['Amount']['Value']) + currency = assert_data['Transaction']['Amount']['CurrencyCode'] + if ( + to_small_currency(expected_amount, expected_currency) == amount + and expected_currency == currency + ): return True current_plugin.logger.warning( "Payment doesn't match events fee: %s %s != %s %s", - amount, currency, to_small_currency(expected_amount, expected_currency), expected_currency + amount, currency, + to_small_currency(expected_amount, expected_currency), + expected_currency + ) + notify_amount_inconsistency( + self.registration, + to_large_currency(amount, currency), + currency ) - notify_amount_inconsistency(self.registration, to_large_currency(amount, currency), currency) return False - def _confirm_transaction(self, transaction_data): - """Confirm to SixPay that the transaction is accepted""" - completion_data = {'ACCOUNTID': transaction_data['ACCOUNTID'], 'ID': transaction_data['ID']} - if 'test.saferpay.com' in self.sixpay_url: - # password: see "Saferpay Payment Page" specification, v5.1, section 4.6 - completion_data['spPassword'] = '8e7Yn5yk' - completion_response = self._perform_request('confirmation', 'PayCompleteV2.asp', **completion_data) - assert completion_response.startswith('OK') - return True + def _capture_transaction(self, assert_data): + """Confirm to SixPay that the transaction is accepted. - def _register_transaction(self, transaction_data): - """Register the transaction persistently within Indico""" - register_transaction( - registration=self.registration, - # SixPay uses SMALLEST currency, Indico expects LARGEST currency - amount=to_large_currency(float(transaction_data['AMOUNT']), transaction_data['CURRENCY']), - currency=transaction_data['CURRENCY'], - action=TransactionAction.complete, - provider='sixpay', - data=transaction_data, + On success returns the response JSON data. + """ + capture_data = { + 'RequestHeader': get_request_header( + saferpay_json_api_spec, self._get_setting('account_id') + ), + 'TransactionReference': { + 'TransactionId': assert_data['Transaction']['Id'] + } + } + capture_response = self._perform_request( + 'capture', saferpay_pp_capture_url, capture_data ) + return capture_response.json() + + def _cancel_transaction(self, assert_data): + """Inform Sixpay that the transaction is canceled. + + Cancel the transaction at Sixpay. This method is implemented but + not used and tested yet. + """ + cancel_data = { + 'RequestHeader': get_request_header( + saferpay_json_api_spec, self._get_setting('account_id') + ), + 'TransactionReference': { + 'TransactionId': assert_data['Transaction']['Id'] + } + } + cancel_response = self._perform_request( + 'cancel', saferpay_pp_cancel_url, cancel_data + ) + return cancel_response.json() + + def _register_payment(self, assert_data): + """Register the transaction as paid.""" + self.registration.transaction.create_next( + registration=self.registration, + amount=self.registration.transaction.amount, + currency=self.registration.transaction.currency, + action=TransactionAction.complete, + provider=provider + ) + self.registration.update_state(paid=True) class UserCancelHandler(BaseRequestHandler): - """User Message on cancelled payment""" + """User Message on cancelled payment.""" + def _process(self): + self.registration.transaction.create_next( + registration=self.registration, + amount=self.registration.transaction.amount, + currency=self.registration.transaction.currency, + action=TransactionAction.reject, + provider=provider + ) flash(gettext('You cancelled the payment.'), 'info') - return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + return redirect(url_for( + 'event_registration.display_regform', + self.registration.locator.registrant) + ) class UserFailureHandler(BaseRequestHandler): - """User Message on failed payment""" + """User Message on failed payment.""" + def _process(self): + self.registration.transaction.create_next( + registration=self.registration, + amount=self.registration.transaction.amount, + currency=self.registration.transaction.currency, + action=TransactionAction.reject, + provider=provider + ) flash(gettext('Your payment has failed.'), 'info') - return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + return redirect(url_for( + 'event_registration.display_regform', + self.registration.locator.registrant) + ) class UserSuccessHandler(SixPayResponseHandler): - """User Message on successful payment""" + """User Message on successful payment.""" + def _process(self): try: self._process_confirmation() except TransactionFailure as err: - current_plugin.logger.warning("SixPay transaction failed during %s: %s" % (err.step, err.details)) - flash(gettext('Your payment could not be confirmed. Please contact an organizer.'), 'info') + current_plugin.logger.warning( + "SixPay transaction failed during %s: %s" % ( + err.step, err.details + ) + ) + flash(gettext( + 'Your payment could not be confirmed.' + ' Please contact an organizer.'), + 'info' + ) else: flash(gettext('Your payment has been confirmed.'), 'success') - return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + return redirect(url_for( + 'event_registration.display_regform', + self.registration.locator.registrant) + ) diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_sixpay/utility.py index b89feb6..ed7f45e 100644 --- a/payment_sixpay/indico_sixpay/utility.py +++ b/payment_sixpay/indico_sixpay/utility.py @@ -1,38 +1,52 @@ # -*- coding: utf-8 -*- -## -## This file is part of the SixPay Indico EPayment Plugin. -## Copyright (C) 2017 - 2018 Max Fischer -## -## This is free software; you can redistribute it and/or -## modify it under the terms of the GNU General Public License as -## published by the Free Software Foundation; either version 3 of the -## License, or (at your option) any later version. -## -## This software is distributed in the hope that it will be useful, but -## WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -## General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with SixPay Indico EPayment Plugin;if not, see . +# +# This file is part of the SixPay Indico EPayment Plugin. +# Copyright (C) 2017 - 2018 Max Fischer +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SixPay Indico EPayment Plugin; +# if not, see . +"""Utility functions used by the Sixpay payment plugin.""" from __future__ import unicode_literals, division +import uuid + import iso4217 from werkzeug.exceptions import NotImplemented as HTTPNotImplemented +from flask_pluginengine import current_plugin from indico.util.i18n import make_bound_gettext #: internationalisation/localisation of strings gettext = make_bound_gettext('payment_sixpay') +# Saferpay API details +saferpay_json_api_spec = '1.12' +saferpay_pp_init_url = 'Payment/v1/PaymentPage/Initialize' +saferpay_pp_assert_url = 'Payment/v1/PaymentPage/Assert' +saferpay_pp_capture_url = 'Payment/v1/Transaction/Capture' +saferpay_pp_cancel_url = 'Payment/v1/Transaction/Cancel' -#: currencies for which the major to minor currency ratio is not a multiple of 10 +# provider string +provider = 'sixpay' + +# currencies for which the major to minor currency ratio +# is not a multiple of 10 NON_DECIMAL_CURRENCY = {'MRU', 'MGA'} def validate_currency(iso_code): - """ - Check whether the currency can be properly handled by this plugin + """Check whether the currency can be properly handled by this plugin. :param iso_code: an ISO4217 currency code, e.g. ``"EUR"`` :type iso_code: basestring @@ -40,19 +54,24 @@ def validate_currency(iso_code): """ if iso_code in NON_DECIMAL_CURRENCY: raise HTTPNotImplemented( - gettext("Unsupported currency '{0}' for SixPay. Please contact the organisers").format(iso_code) + gettext( + "Unsupported currency '{0}' for SixPay." + " Please contact the organisers" + ).format(iso_code) ) try: iso4217.Currency(iso_code) except ValueError: raise HTTPNotImplemented( - gettext("Unknown currency '{0}' for SixPay. Please contact the organisers").format(iso_code) + gettext( + "Unknown currency '{0}' for SixPay." + " Please contact the organisers" + ).format(iso_code) ) def to_small_currency(large_currency_amount, iso_code): - """ - Convert an amount from large currency to small currency, e.g. 2.3 Euro to 230 Eurocent + """Convert an amount from large currency to small currency. :param large_currency_amount: the amount in large currency, e.g. ``2.3`` :param iso_code: the ISO currency code, e.g. ``"EUR"`` @@ -62,13 +81,59 @@ def to_small_currency(large_currency_amount, iso_code): exponent = iso4217.Currency(iso_code).exponent if exponent == 0: return large_currency_amount - return large_currency_amount * (10 ** exponent) + return int(large_currency_amount * (10 ** exponent)) def to_large_currency(small_currency_amount, iso_code): - """Inverse of :py:func:`to_small_currency`""" + """Inverse of :py:func:`to_small_currency`.""" validate_currency(iso_code) exponent = iso4217.Currency(iso_code).exponent if exponent == 0: return small_currency_amount return small_currency_amount / (10 ** exponent) + + +def get_request_header(api_spec, account_id): + """Return request header dict. + + Contained information: + - SpecVersion + - CustomerId + - RequestId + - RetryIndicator + """ + request_header = { + 'SpecVersion': api_spec, + 'CustomerId': get_customer_id(account_id), + 'RequestId': str(uuid.uuid4()), + 'RetryIndicator': 0, + } + return request_header + + +def get_customer_id(account_id): + """Extract customer ID from account ID. + + Customer ID is the first part (befor the hyphen) of the account ID. + """ + return account_id.split('-')[0] + + +def get_terminal_id(account_id): + """Extract the teminal ID from account ID. + + The Terminal ID is the second part (after the hyphen) of the + account ID. + """ + return account_id.split('-')[1] + + +def get_setting(setting, event=None): + """Return a configuration setting of the plugin.""" + if event: + return ( + current_plugin.event_settings.get(event, setting) + or current_plugin.settings.get(setting) + ) + else: + return current_plugin.settings.get(setting) From a75f48a034ce5b3f66beaf11fc17962fa82dfeaf Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:19:17 +0200 Subject: [PATCH 03/14] Add dev tool configs --- payment_sixpay/.flake8 | 29 +++++++++++++++++++++++++++++ payment_sixpay/.isort.cfg | 8 ++++++++ 2 files changed, 37 insertions(+) create mode 100644 payment_sixpay/.flake8 create mode 100644 payment_sixpay/.isort.cfg diff --git a/payment_sixpay/.flake8 b/payment_sixpay/.flake8 new file mode 100644 index 0000000..b7cdb63 --- /dev/null +++ b/payment_sixpay/.flake8 @@ -0,0 +1,29 @@ +[flake8] +max-line-length = 120 + +# colored output +format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s + +# decent quote styles +inline-quotes = single +multiline-quotes = single +docstring-quotes = double +avoid-escape = true + +extend-exclude = + build + dist + docs + htmlcov + *.egg-info + node_modules + .*/ + +ignore = + # allow omitting whitespace around arithmetic operators + E226 + # don't require specific wrapping before/after binary operators + W503 + W504 + # allow assigning lambdas (it's useful for single-line functions defined inside other functions) + E731 diff --git a/payment_sixpay/.isort.cfg b/payment_sixpay/.isort.cfg new file mode 100644 index 0000000..d780987 --- /dev/null +++ b/payment_sixpay/.isort.cfg @@ -0,0 +1,8 @@ +[isort] +line_length=120 +multi_line_output=0 +lines_after_imports=2 +sections=FUTURE,STDLIB,THIRDPARTY,INDICO,FIRSTPARTY,LOCALFOLDER +known_third_party=flask_multipass,flask_pluginengine +known_indico=indico +skip_glob=20??????????_*_*.py From e9b4bc874dd45b0ddc4c53c7dfe02b06b9752629 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:19:30 +0200 Subject: [PATCH 04/14] Bump version --- payment_sixpay/indico_sixpay/__about__.py | 2 +- payment_sixpay/setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/payment_sixpay/indico_sixpay/__about__.py b/payment_sixpay/indico_sixpay/__about__.py index f45b463..20f0d33 100644 --- a/payment_sixpay/indico_sixpay/__about__.py +++ b/payment_sixpay/indico_sixpay/__about__.py @@ -48,7 +48,7 @@ __title__ = 'indico_sixpay' __summary__ = 'Indico EPayment Plugin for SixPay services' __url__ = 'https://github.com/maxfischer2781/indico_sixpay' -__version__ = '2.0.2' +__version__ = '3.0-dev' __author__ = 'Max Fischer' __email__ = 'maxfischer2781@gmail.com' __copyright__ = '2017 - 2018 %s' % __author__ diff --git a/payment_sixpay/setup.py b/payment_sixpay/setup.py index fc9e88a..2c80685 100644 --- a/payment_sixpay/setup.py +++ b/payment_sixpay/setup.py @@ -22,7 +22,8 @@ setup( }, packages=find_packages(), package_data={'indico_sixpay': ['templates/*.html']}, - install_requires=['requests', 'indico>=2.0', 'iso4217'], + install_requires=['requests', 'indico>=3.0', 'iso4217'], + python_requires='~=3.9.0', license='GPLv3+', classifiers=[ 'Development Status :: 5 - Production/Stable', From ae8f85dde718bceb35da48c2cbd729334dd02335 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:21:03 +0200 Subject: [PATCH 05/14] Run isort --- payment_sixpay/indico_sixpay/blueprint.py | 5 +-- payment_sixpay/indico_sixpay/plugin.py | 36 ++++++------------- .../indico_sixpay/request_handlers.py | 23 ++++-------- payment_sixpay/indico_sixpay/utility.py | 5 +-- payment_sixpay/setup.py | 4 ++- 5 files changed, 25 insertions(+), 48 deletions(-) diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py index 8d48893..1bbae7e 100644 --- a/payment_sixpay/indico_sixpay/blueprint.py +++ b/payment_sixpay/indico_sixpay/blueprint.py @@ -21,10 +21,7 @@ from __future__ import unicode_literals from indico.core.plugins import IndicoPluginBlueprint -from .request_handlers import ( - SixPayResponseHandler, UserCancelHandler, - UserFailureHandler, UserSuccessHandler -) +from .request_handlers import SixPayResponseHandler, UserCancelHandler, UserFailureHandler, UserSuccessHandler #: url mount points exposing callbacks diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py index c93099c..9e4bf0e 100644 --- a/payment_sixpay/indico_sixpay/plugin.py +++ b/payment_sixpay/indico_sixpay/plugin.py @@ -22,41 +22,27 @@ The entry point for indico is the :py:class:`~.SixpayPaymentPlugin`. It handles configuration via the settings forms, initiates payments and provides callbacks for finished payments via its blueprint. """ -from __future__ import unicode_literals, absolute_import -import urlparse +from __future__ import absolute_import, unicode_literals import requests -from werkzeug.exceptions import ( - NotImplemented as HTTPNotImplemented, - InternalServerError as HTTPInternalServerError -) - +import urlparse +from werkzeug.exceptions import InternalServerError as HTTPInternalServerError +from werkzeug.exceptions import NotImplemented as HTTPNotImplemented from wtforms.fields import StringField from wtforms.fields.html5 import URLField -from wtforms.validators import ( - DataRequired, Optional, Regexp, Length, Email, ValidationError -) -from indico.web.forms.fields import IndicoPasswordField +from wtforms.validators import DataRequired, Email, Length, Optional, Regexp, ValidationError from indico.core.plugins import IndicoPlugin, url_for_plugin -from indico.modules.events.payment import ( - PaymentEventSettingsFormBase, - PaymentPluginMixin, - PaymentPluginSettingsFormBase) -from indico.modules.events.payment.models.transactions import ( - PaymentTransaction, - TransactionAction -) +from indico.modules.events.payment import (PaymentEventSettingsFormBase, PaymentPluginMixin, + PaymentPluginSettingsFormBase) +from indico.modules.events.payment.models.transactions import PaymentTransaction, TransactionAction +from indico.web.forms.fields import IndicoPasswordField - -from .utility import ( - gettext, to_small_currency, get_request_header, get_terminal_id, - provider -) # blueprint mounts the request handlers onto URLs from .blueprint import blueprint +from .utility import (get_request_header, get_terminal_id, gettext, provider, saferpay_json_api_spec, + saferpay_pp_init_url, to_small_currency) -from .utility import saferpay_json_api_spec, saferpay_pp_init_url # Dear Future Maintainer, # diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py index 3b7d4a4..4e9a8a6 100644 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -18,32 +18,23 @@ # if not, see . """Callbacks for asynchronous replies by Saferpay and to redirect the user.""" from __future__ import unicode_literals -import urlparse import requests +import urlparse from flask import flash, redirect, request from flask_pluginengine import current_plugin from werkzeug.exceptions import BadRequest from indico.modules.events.payment.models.transactions import TransactionAction -from indico.modules.events.payment.notifications \ - import notify_amount_inconsistency -from indico.modules.events.registration.models.registrations \ - import Registration +from indico.modules.events.payment.notifications import notify_amount_inconsistency +from indico.modules.events.registration.models.registrations import Registration from indico.web.flask.util import url_for from indico.web.rh import RH -from .utility import ( - gettext, to_large_currency, to_small_currency, get_request_header, - get_setting -) -from .utility import ( - saferpay_pp_assert_url, - saferpay_pp_capture_url, - saferpay_json_api_spec, - saferpay_pp_cancel_url, - provider -) +from .utility import (get_request_header, get_setting, gettext, provider, saferpay_json_api_spec, + saferpay_pp_assert_url, saferpay_pp_cancel_url, saferpay_pp_capture_url, to_large_currency, + to_small_currency) + # RH from indico.web.rh # - the logic to execute when SixPay/Users are redirected *after* a transaction diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_sixpay/utility.py index ed7f45e..5234bab 100644 --- a/payment_sixpay/indico_sixpay/utility.py +++ b/payment_sixpay/indico_sixpay/utility.py @@ -17,16 +17,17 @@ # along with SixPay Indico EPayment Plugin; # if not, see . """Utility functions used by the Sixpay payment plugin.""" -from __future__ import unicode_literals, division +from __future__ import division, unicode_literals import uuid import iso4217 -from werkzeug.exceptions import NotImplemented as HTTPNotImplemented from flask_pluginengine import current_plugin +from werkzeug.exceptions import NotImplemented as HTTPNotImplemented from indico.util.i18n import make_bound_gettext + #: internationalisation/localisation of strings gettext = make_bound_gettext('payment_sixpay') diff --git a/payment_sixpay/setup.py b/payment_sixpay/setup.py index 2c80685..741e51b 100644 --- a/payment_sixpay/setup.py +++ b/payment_sixpay/setup.py @@ -1,5 +1,7 @@ import os -from setuptools import setup, find_packages + +from setuptools import find_packages, setup + repo_base_dir = os.path.abspath(os.path.dirname(__file__)) # pull in the packages metadata From f2eabd112b6e49f04d245d6bbdee32396c871989 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:28:50 +0200 Subject: [PATCH 06/14] Fix flake8 issues --- payment_sixpay/indico_sixpay/__init__.py | 32 +++++++++---------- payment_sixpay/indico_sixpay/plugin.py | 31 +++++------------- .../indico_sixpay/request_handlers.py | 10 +++--- payment_sixpay/indico_sixpay/utility.py | 4 +-- payment_sixpay/setup.py | 2 +- 5 files changed, 32 insertions(+), 47 deletions(-) diff --git a/payment_sixpay/indico_sixpay/__init__.py b/payment_sixpay/indico_sixpay/__init__.py index ace9ae6..592baf9 100644 --- a/payment_sixpay/indico_sixpay/__init__.py +++ b/payment_sixpay/indico_sixpay/__init__.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- -## -## This file is part of the SixPay Indico EPayment Plugin. -## Copyright (C) 2017 - 2018 Max Fischer -## -## This is free software; you can redistribute it and/or -## modify it under the terms of the GNU General Public License as -## published by the Free Software Foundation; either version 3 of the -## License, or (at your option) any later version. -## -## This software is distributed in the hope that it will be useful, but -## WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -## General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with SixPay Indico EPayment Plugin;if not, see . +# +# This file is part of the SixPay Indico EPayment Plugin. +# Copyright (C) 2017 - 2018 Max Fischer +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SixPay Indico EPayment Plugin;if not, see . diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py index 9e4bf0e..15d08c4 100644 --- a/payment_sixpay/indico_sixpay/plugin.py +++ b/payment_sixpay/indico_sixpay/plugin.py @@ -25,7 +25,7 @@ and provides callbacks for finished payments via its blueprint. from __future__ import absolute_import, unicode_literals import requests -import urlparse +from urllib.parse import urljoin from werkzeug.exceptions import InternalServerError as HTTPInternalServerError from werkzeug.exceptions import NotImplemented as HTTPNotImplemented from wtforms.fields import StringField @@ -182,7 +182,7 @@ class PluginSettingsForm(PaymentPluginSettingsFormBase): validators=[Optional(), Email(), Length(0, 50)], description=gettext( 'Mail address to receive notifications of transactions.' - 'This is independent of Indico\'s own payment notifications.' + "This is independent of Indico's own payment notifications." ) ) @@ -191,9 +191,6 @@ class EventSettingsForm(PaymentEventSettingsFormBase): """Configuration form for the Plugin for a specific event.""" # every setting may be overwritten for each event - #url = PluginSettingsForm.url - #username = PluginSettingsForm.username - #password = PluginSettingsForm.password account_id = PluginSettingsForm.account_id order_description = PluginSettingsForm.order_description order_identifier = PluginSettingsForm.order_identifier @@ -318,23 +315,11 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): plugin_settings[format_field].format(**format_map) ) except ValueError: - message = ( - "Invalid format field placeholder for {0}," - " please contact the event organisers!" - ) - raise HTTPNotImplemented( - (gettext(message) + '\n\n[' + message + ']') - .format(self.name) - ) + message = 'Invalid format field placeholder for {0}, please contact the event organisers!' + raise HTTPNotImplemented((gettext(message) + '\n\n[' + message + ']').format(self.name)) except KeyError: - message = ( - 'Unknown format field placeholder "{0}" for {1},' - ' please contact the event organisers!' - ) - raise HTTPNotImplemented(( - gettext(message) + '\n\n[' + message + ']' - ).format(format_field, self.name) - ) + message = 'Unknown format field placeholder "{0}" for {1}, please contact the event organisers!' + raise HTTPNotImplemented((gettext(message) + '\n\n[' + message + ']').format(format_field, self.name)) # see the SixPay Manual # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize @@ -398,7 +383,7 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): def _init_payment_page(self, sixpay_url, transaction_data, credentials): """Initialize payment page.""" - endpoint = urlparse.urljoin(sixpay_url, saferpay_pp_init_url) + endpoint = urljoin(sixpay_url, saferpay_pp_init_url) url_request = requests.post( endpoint, json=transaction_data, @@ -413,6 +398,6 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): raise HTTPInternalServerError( 'Failed request to SixPay service:' ' {ErrorMessage}. {ErrorDetail}' - .format(response) + .format(**response) ) return response diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py index 4e9a8a6..37037b7 100644 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -20,7 +20,7 @@ from __future__ import unicode_literals import requests -import urlparse +from urllib.parse import urljoin from flask import flash, redirect, request from flask_pluginengine import current_plugin from werkzeug.exceptions import BadRequest @@ -98,7 +98,7 @@ class SixPayResponseHandler(BaseRequestHandler): self._process_confirmation() except TransactionFailure as err: current_plugin.logger.warning( - "SixPay transaction failed during %s: %s" + 'SixPay transaction failed during %s: %s' % (err.step, err.details) ) @@ -123,7 +123,7 @@ class SixPayResponseHandler(BaseRequestHandler): self._register_payment(assert_response) except TransactionFailure as err: current_plugin.logger.warning( - "SixPay transaction failed during %s: %s" + 'SixPay transaction failed during %s: %s' % (err.step, err.details) ) raise @@ -142,7 +142,7 @@ class SixPayResponseHandler(BaseRequestHandler): request. If the request itself fails, a :py:exc:`~.TransactionFailure` is raised for ``task``. """ - request_url = urlparse.urljoin(self.sixpay_url, endpoint) + request_url = urljoin(self.sixpay_url, endpoint) credentials = ( get_setting('username'), get_setting('password') @@ -325,7 +325,7 @@ class UserSuccessHandler(SixPayResponseHandler): self._process_confirmation() except TransactionFailure as err: current_plugin.logger.warning( - "SixPay transaction failed during %s: %s" % ( + 'SixPay transaction failed during %s: %s' % ( err.step, err.details ) ) diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_sixpay/utility.py index 5234bab..cf86594 100644 --- a/payment_sixpay/indico_sixpay/utility.py +++ b/payment_sixpay/indico_sixpay/utility.py @@ -57,7 +57,7 @@ def validate_currency(iso_code): raise HTTPNotImplemented( gettext( "Unsupported currency '{0}' for SixPay." - " Please contact the organisers" + ' Please contact the organisers' ).format(iso_code) ) try: @@ -66,7 +66,7 @@ def validate_currency(iso_code): raise HTTPNotImplemented( gettext( "Unknown currency '{0}' for SixPay." - " Please contact the organisers" + ' Please contact the organisers' ).format(iso_code) ) diff --git a/payment_sixpay/setup.py b/payment_sixpay/setup.py index 741e51b..f662d15 100644 --- a/payment_sixpay/setup.py +++ b/payment_sixpay/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages, setup repo_base_dir = os.path.abspath(os.path.dirname(__file__)) # pull in the packages metadata package_about = {} -with open(os.path.join(repo_base_dir, "indico_sixpay", "__about__.py")) as about_file: +with open(os.path.join(repo_base_dir, 'indico_sixpay', '__about__.py')) as about_file: exec(about_file.read(), package_about) setup( From 614d7369d89e515953085be04d3d7168dbdb9f97 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:29:15 +0200 Subject: [PATCH 07/14] Run pyupgrade --- payment_sixpay/indico_sixpay/__init__.py | 1 - payment_sixpay/indico_sixpay/blueprint.py | 2 -- payment_sixpay/indico_sixpay/plugin.py | 12 +++++------- payment_sixpay/indico_sixpay/request_handlers.py | 8 +++----- payment_sixpay/indico_sixpay/utility.py | 2 -- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/payment_sixpay/indico_sixpay/__init__.py b/payment_sixpay/indico_sixpay/__init__.py index 592baf9..64a76f2 100644 --- a/payment_sixpay/indico_sixpay/__init__.py +++ b/payment_sixpay/indico_sixpay/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # This file is part of the SixPay Indico EPayment Plugin. # Copyright (C) 2017 - 2018 Max Fischer diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py index 1bbae7e..b948f5d 100644 --- a/payment_sixpay/indico_sixpay/blueprint.py +++ b/payment_sixpay/indico_sixpay/blueprint.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # This file is part of the SixPay Indico EPayment Plugin. # Copyright (C) 2017 - 2018 Max Fischer @@ -17,7 +16,6 @@ # along with SixPay Indico EPayment Plugin; # if not, see . """Definition of callbacks exposed by the Indico server.""" -from __future__ import unicode_literals from indico.core.plugins import IndicoPluginBlueprint diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py index 15d08c4..ed14ad5 100644 --- a/payment_sixpay/indico_sixpay/plugin.py +++ b/payment_sixpay/indico_sixpay/plugin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of the SixPay Indico EPayment Plugin. # Copyright (C) 2017 - 2018 Max Fischer # @@ -22,7 +21,6 @@ The entry point for indico is the :py:class:`~.SixpayPaymentPlugin`. It handles configuration via the settings forms, initiates payments and provides callbacks for finished payments via its blueprint. """ -from __future__ import absolute_import, unicode_literals import requests from urllib.parse import urljoin @@ -64,7 +62,7 @@ from .utility import (get_request_header, get_terminal_id, gettext, provider, sa # - validators: Input validation, see wtforms.validators # - description: help text of the field, an internationalised text -class FormatField(object): +class FormatField: """Validator for format fields, i.e. strings with ``{key}`` placeholders. :param max_length: optional maximum length, checked on a test formatting @@ -112,13 +110,13 @@ class FormatField(object): try: test_format = field.data.format(**self.field_map) except KeyError as err: - raise ValidationError('Invalid format string key: {}'.format(err)) + raise ValidationError(f'Invalid format string key: {err}') except ValueError as err: - raise ValidationError('Malformed format string: {}'.format(err)) + raise ValidationError(f'Malformed format string: {err}') if len(test_format) > self.max_length: raise ValidationError( 'Too long format string:' - ' shortest replacement with {0}, expected {1}' + ' shortest replacement with {}, expected {}' .format( len(test_format), self.max_length ) @@ -301,7 +299,7 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): 'event_id': registration.event_id, 'event_title': registration.event.title, 'eventuser_id': - 'e{0}u{1}'.format(registration.event_id, registration.user_id), + f'e{registration.event_id}u{registration.user_id}', 'registration_title': registration.registration_form.title } diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py index 37037b7..98a2786 100644 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # This file is part of the SixPay Indico EPayment Plugin. # Copyright (C) 2017 - 2018 Max Fischer @@ -17,7 +16,6 @@ # along with SixPay Indico EPayment Plugin; # if not, see . """Callbacks for asynchronous replies by Saferpay and to redirect the user.""" -from __future__ import unicode_literals import requests from urllib.parse import urljoin @@ -84,12 +82,12 @@ class SixPayResponseHandler(BaseRequestHandler): def __init__(self): """Initialize request handler.""" - super(SixPayResponseHandler, self).__init__() + super().__init__() # registration context is not initialised before `self._process_args` self.sixpay_url = None # type: str def _process_args(self): - super(SixPayResponseHandler, self)._process_args() + super()._process_args() self.sixpay_url = get_setting('url') def _process(self): @@ -325,7 +323,7 @@ class UserSuccessHandler(SixPayResponseHandler): self._process_confirmation() except TransactionFailure as err: current_plugin.logger.warning( - 'SixPay transaction failed during %s: %s' % ( + 'SixPay transaction failed during {}: {}'.format( err.step, err.details ) ) diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_sixpay/utility.py index cf86594..613630c 100644 --- a/payment_sixpay/indico_sixpay/utility.py +++ b/payment_sixpay/indico_sixpay/utility.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # This file is part of the SixPay Indico EPayment Plugin. # Copyright (C) 2017 - 2018 Max Fischer @@ -17,7 +16,6 @@ # along with SixPay Indico EPayment Plugin; # if not, see . """Utility functions used by the Sixpay payment plugin.""" -from __future__ import division, unicode_literals import uuid From 6805da207bbe9bf40a1fdd6c7020f50634ac715c Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:30:04 +0200 Subject: [PATCH 08/14] Use standard plugin package name --- payment_sixpay/indico_sixpay/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payment_sixpay/indico_sixpay/__about__.py b/payment_sixpay/indico_sixpay/__about__.py index 20f0d33..424da96 100644 --- a/payment_sixpay/indico_sixpay/__about__.py +++ b/payment_sixpay/indico_sixpay/__about__.py @@ -44,7 +44,7 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMA ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -__title__ = 'indico_sixpay' +__title__ = 'indico-plugin-sixpay' __summary__ = 'Indico EPayment Plugin for SixPay services' __url__ = 'https://github.com/maxfischer2781/indico_sixpay' From ef8885a80ce90e20c8cf39fab810c1b7b6d7f728 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 12:53:07 +0200 Subject: [PATCH 09/14] Adapt to Indico 3.0 --- payment_sixpay/indico_sixpay/blueprint.py | 5 +---- payment_sixpay/indico_sixpay/request_handlers.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py index b948f5d..b24f55d 100644 --- a/payment_sixpay/indico_sixpay/blueprint.py +++ b/payment_sixpay/indico_sixpay/blueprint.py @@ -25,10 +25,7 @@ from .request_handlers import SixPayResponseHandler, UserCancelHandler, UserFail #: url mount points exposing callbacks blueprint = IndicoPluginBlueprint( 'payment_sixpay', __name__, - url_prefix=( - '/event//registrations/' - '/payment/response/sixpay' - ) + url_prefix='/event//registrations//payment/response/sixpay' ) blueprint.add_url_rule( diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py index 98a2786..d0585d7 100644 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -51,7 +51,7 @@ class BaseRequestHandler(RH): CSRF_ENABLED = False def _process_args(self): - self.registration = Registration.find_first(uuid=request.args['token']) + self.registration = Registration.query.filter_by(uuid=request.args['token']).first() if not self.registration: raise BadRequest self.token = ( From 5caa68e70f1fc3d87f8d23dc0cead4e611a81c99 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 14:44:30 +0200 Subject: [PATCH 10/14] Remove docs Parts of it are outdated, others are covered by the generic indico plugin setup docs... --- payment_sixpay/build_docs.sh | 23 --- payment_sixpay/docs/Makefile | 20 -- payment_sixpay/docs/conf.py | 191 ------------------ payment_sixpay/docs/index.rst | 99 --------- .../source/api/indico_sixpay.blueprint.rst | 7 - .../docs/source/api/indico_sixpay.plugin.rst | 7 - .../api/indico_sixpay.request_handlers.rst | 7 - .../docs/source/api/indico_sixpay.rst | 18 -- .../docs/source/api/indico_sixpay.utility.rst | 7 - payment_sixpay/docs/source/api/modules.rst | 7 - payment_sixpay/docs/source/changelog.rst | 35 ---- payment_sixpay/docs/source/configuration.rst | 121 ----------- payment_sixpay/docs/source/design.rst | 24 --- .../docs/source/images/uml/transaction.svg | 58 ------ payment_sixpay/docs/source/installation.rst | 59 ------ .../docs/source/uml/transaction.uml | 45 ----- 16 files changed, 728 deletions(-) delete mode 100755 payment_sixpay/build_docs.sh delete mode 100644 payment_sixpay/docs/Makefile delete mode 100644 payment_sixpay/docs/conf.py delete mode 100644 payment_sixpay/docs/index.rst delete mode 100644 payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst delete mode 100644 payment_sixpay/docs/source/api/indico_sixpay.plugin.rst delete mode 100644 payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst delete mode 100644 payment_sixpay/docs/source/api/indico_sixpay.rst delete mode 100644 payment_sixpay/docs/source/api/indico_sixpay.utility.rst delete mode 100644 payment_sixpay/docs/source/api/modules.rst delete mode 100644 payment_sixpay/docs/source/changelog.rst delete mode 100644 payment_sixpay/docs/source/configuration.rst delete mode 100644 payment_sixpay/docs/source/design.rst delete mode 100644 payment_sixpay/docs/source/images/uml/transaction.svg delete mode 100644 payment_sixpay/docs/source/installation.rst delete mode 100644 payment_sixpay/docs/source/uml/transaction.uml diff --git a/payment_sixpay/build_docs.sh b/payment_sixpay/build_docs.sh deleted file mode 100755 index d11e7ff..0000000 --- a/payment_sixpay/build_docs.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# Perform a complete build of the documentation -set -e - -LIB_NAME=indico_sixpay -DOCS_DIR=docs - -cd ${DOCS_DIR} -# cleanup backed files -touch source/api/dummy -rm source/api/* -if which plantuml >/dev/null -then - echo "Building UML images..." - touch source/images/uml/dummy - rm source/images/uml/* - plantuml -tsvg -o ../images/uml/ source/uml/*.uml -fi - -# sphinx build -sphinx-apidoc --module-first --separate --output-dir=source/api ../${LIB_NAME} --force && \ -python2 $(which sphinx-build) -b html -d build/doctrees . build/html/ && \ -open build/html/index.html diff --git a/payment_sixpay/docs/Makefile b/payment_sixpay/docs/Makefile deleted file mode 100644 index 2beb2ea..0000000 --- a/payment_sixpay/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = chainlet -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/payment_sixpay/docs/conf.py b/payment_sixpay/docs/conf.py deleted file mode 100644 index e000baa..0000000 --- a/payment_sixpay/docs/conf.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# indico_sixpay documentation build configuration file, created by -# sphinx-quickstart on Wed Feb 22 14:45:32 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -import sphinx.ext.autodoc as autodoc -# sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from indico_sixpay import __about__ - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.imgmath', - 'sphinx.ext.githubpages', - 'sphinx.ext.graphviz', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = __about__.__title__ -copyright = __about__.__copyright__ -author = __about__.__author__ - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __about__.__version__ -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -# html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'chainletdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'chainlet.tex', 'chainlet Documentation', - 'Max Fischer', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'chainlet', 'chainlet Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'chainlet', 'chainlet Documentation', - author, 'chainlet', 'One line description of project.', - 'Miscellaneous'), -] - -# plugin and extensions -intersphinx_mapping = {'https://docs.python.org/3/': None} - - -def skip_pickle_inject(app, what, name, obj, skip, options): - """skip global wrapper._raw_slave names used only for pickle support""" - if name.endswith('._raw_slave'): - return True - return None - - -def wraplet_signature(app, what, name, obj, options, signature, return_annotation): - """have wrapplets use the signature of the slave""" - try: - wrapped = obj._raw_slave - except AttributeError: - return None - else: - slave_argspec = autodoc.getargspec(wrapped) - slave_signature = autodoc.formatargspec(obj, *slave_argspec) - return (slave_signature, return_annotation) - - -def setup(app): - app.connect('autodoc-skip-member', skip_pickle_inject) - app.connect('autodoc-process-signature', wraplet_signature) diff --git a/payment_sixpay/docs/index.rst b/payment_sixpay/docs/index.rst deleted file mode 100644 index 8da10c6..0000000 --- a/payment_sixpay/docs/index.rst +++ /dev/null @@ -1,99 +0,0 @@ -================================================== -``indico_sixpay`` - SIX EPayment Plugin for Indico -================================================== - -.. image:: https://readthedocs.org/projects/indico_sixpay/badge/?version=latest - :target: http://indico-sixpay.readthedocs.io/en/latest/?badge=latest - :alt: Documentation - -.. image:: https://img.shields.io/pypi/v/indico_sixpay.svg - :alt: Available on PyPI - :target: https://pypi.python.org/pypi/indico_sixpay/ - -.. image:: https://img.shields.io/github/license/maxfischer2781/indico_sixpay.svg - :alt: License - :target: https://github.com/maxfischer2781/indico_sixpay/blob/master/LICENSE - -.. image:: https://img.shields.io/github/commits-since/maxfischer2781/indico_sixpay/v2.0.1.svg - :alt: Repository - :target: https://github.com/maxfischer2781/indico_sixpay/tree/master - -.. toctree:: - :maxdepth: 1 - :caption: Subtopics Overview: - - source/installation - source/configuration - source/changelog - source/design - Module Index - -The :py:mod:`indico_sixpay` adds an EPayment option for -the *SIX Payment Services* provider -to the *Indico* event management system. - -Overview --------- - -If the plugin is enabled, event participants can select the ``SixPay`` payment method during the EPayment checkout. -Payment is performed via the **Saferpay Payment Page**, an external service provided by *SIX Payment Services*. -The plugin handles the user interaction inside Indico, and the secure, asynchronous transaction with SIX Payment Services. - -The plugin must be installed for an entire Indico instance. -It can be enabled and configured for the entire instance and per individual event. -Note that a valid account with *SIX Payment Services* is required to receive payments. - -The plugin follows the -`Saferpay Payment Page `_ -specification version ``5.1``. - -*This plugin supports Indico 2.0.* -*The legacy plugin for Indico 1.2 is hosted on* `github `_. - -Quick Guide ------------ - -To enable the plugin, it must be installed for the python version running ``indico``. - -.. code:: bash - - python -m pip install indico_sixpay - -Once installed, it can be enabled in the administrator and event settings. -Configuration uses the same options for global defaults and event specific overrides. - -Usage Notes ------------ - -The plugin relies on the ISO 4217 standard for currency conversions. -Since they are not properly covered by the standard, the currencies ``MGA`` and ``MRU`` cannot be used for payments. - -Contributing and Feedback -------------------------- - -The project is hosted on `github `_. -Feedback, pull requests and other contributions are always welcome. -If you have issues or suggestions, check the issue tracker: |issues| - -Disclaimer ----------- - -This plugin is in no way endorsed, supported or provided by SIX, Indico or any other service, provider or entity. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - ----------- - -.. |issues| image:: https://img.shields.io/github/issues-raw/maxfischer2781/indico_sixpay.svg - :target: https://github.com/maxfischer2781/indico_sixpay/issues - :alt: Open Issues - -Documentation built from ``indico_sixpay`` |version| at |today|. diff --git a/payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst b/payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst deleted file mode 100644 index 5698cb4..0000000 --- a/payment_sixpay/docs/source/api/indico_sixpay.blueprint.rst +++ /dev/null @@ -1,7 +0,0 @@ -indico\_sixpay\.blueprint module -================================ - -.. automodule:: indico_sixpay.blueprint - :members: - :undoc-members: - :show-inheritance: diff --git a/payment_sixpay/docs/source/api/indico_sixpay.plugin.rst b/payment_sixpay/docs/source/api/indico_sixpay.plugin.rst deleted file mode 100644 index a58facb..0000000 --- a/payment_sixpay/docs/source/api/indico_sixpay.plugin.rst +++ /dev/null @@ -1,7 +0,0 @@ -indico\_sixpay\.plugin module -============================= - -.. automodule:: indico_sixpay.plugin - :members: - :undoc-members: - :show-inheritance: diff --git a/payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst b/payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst deleted file mode 100644 index 7f6dc90..0000000 --- a/payment_sixpay/docs/source/api/indico_sixpay.request_handlers.rst +++ /dev/null @@ -1,7 +0,0 @@ -indico\_sixpay\.request\_handlers module -======================================== - -.. automodule:: indico_sixpay.request_handlers - :members: - :undoc-members: - :show-inheritance: diff --git a/payment_sixpay/docs/source/api/indico_sixpay.rst b/payment_sixpay/docs/source/api/indico_sixpay.rst deleted file mode 100644 index d42503a..0000000 --- a/payment_sixpay/docs/source/api/indico_sixpay.rst +++ /dev/null @@ -1,18 +0,0 @@ -indico\_sixpay package -====================== - -.. automodule:: indico_sixpay - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - - indico_sixpay.blueprint - indico_sixpay.plugin - indico_sixpay.request_handlers - indico_sixpay.utility - diff --git a/payment_sixpay/docs/source/api/indico_sixpay.utility.rst b/payment_sixpay/docs/source/api/indico_sixpay.utility.rst deleted file mode 100644 index dfdc9b3..0000000 --- a/payment_sixpay/docs/source/api/indico_sixpay.utility.rst +++ /dev/null @@ -1,7 +0,0 @@ -indico\_sixpay\.utility module -============================== - -.. automodule:: indico_sixpay.utility - :members: - :undoc-members: - :show-inheritance: diff --git a/payment_sixpay/docs/source/api/modules.rst b/payment_sixpay/docs/source/api/modules.rst deleted file mode 100644 index 1f26b88..0000000 --- a/payment_sixpay/docs/source/api/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -indico_sixpay -============= - -.. toctree:: - :maxdepth: 4 - - indico_sixpay diff --git a/payment_sixpay/docs/source/changelog.rst b/payment_sixpay/docs/source/changelog.rst deleted file mode 100644 index 0b43398..0000000 --- a/payment_sixpay/docs/source/changelog.rst +++ /dev/null @@ -1,35 +0,0 @@ -+++++++++ -Changelog -+++++++++ - - -v2.0.2 -- 2018-05-23 --------------------- - - * Default SaferPay URL includes a trailing slash to form correct URLs. - -v2.0.1 -- 2018-03-01 --------------------- - - * event settings override global settings (`issue #6 `_) - -v2.0.0 -- 2018-02-08 --------------------- - - * public release for Indico 2.0 - -v1.2.2 -- 2017-07-10 --------------------- - - * internal identifier for transactions is configurable - * expanded order description placeholders - -v1.2.1 -- 2017-03-14 --------------------- - - * bugfix for duplicate transaction verification - -v1.2.0 -- 2017-03-13 --------------------- - - * public release for Indico 1.2 diff --git a/payment_sixpay/docs/source/configuration.rst b/payment_sixpay/docs/source/configuration.rst deleted file mode 100644 index 9367f7e..0000000 --- a/payment_sixpay/docs/source/configuration.rst +++ /dev/null @@ -1,121 +0,0 @@ -Plugin Configuration -==================== - -The plugin must be installed for an entire Indico instance. -It can be enabled and configured for the entire instance and per individual event. -Both levels have the same configuration options: -The global settings act as a default, and are overridden by event specific settings. - -Configuration Options ---------------------- - -**SixPay Saferpay URL** - - The URL to contact the Six Payment Service. - Use the default ``https://www.saferpay.com/hosting/`` for any transaction. - For testing, use the ``https://test.saferpay.com/hosting/`` test service. - - You should generally *not* change this, unless you want to test the plugin. - If the official saferpay URL changes, please submit an `issue ticket`_. - -**Account ID** - - The ID of your Saferpay account, a number containing slashes. - For testing, use the ID ``401860-17795278``. - - This ID is provided to you by Six Payment Services. - -**Order Description** [80 characters] - - The description of each order in a human readable way. - This description is presented to the registrant during the transaction with SixPay. - - This field is limited to 80 characters, after any placeholders are filled in. - The suggested length is 50 characters. - The default description uses the registrant name and event title. - -**Order Identifier** [80 characters] - - The identifier of each order for further processing. - This identifier is used internally and in your own billing. - - This field is stripped of whitespace and limited to 80 characters, after any placeholders are filled in. - Note that auxiliary services, e.g. for billing, may limit this information to 12 characters. - -**Notification Mail** - - Mail address to receive notifications of transactions. - This is independent of Indico's own payment notifications. - -Format Placeholders -------------------- - -The **Order Description/Identifier** settings allow for placeholders. -These are dynamically filled in for each event and registrant. - -``{user_id}`` [`231`] - - Numerical identifier of the user/registrant. - This is unique per event, but not globally unique. - -``{user_name}`` [`Jane Doe`] - - Full name of the user/registrant. - Use `` `` format. - -``{user_firstname}`` [`Jane`] - - First name of the user/registrant. - -``{user_lastname}`` [`Doe`] - - Last name of the user/registrant. - -``{event_id}`` [`18`] - - Numerical identifier of the event. - This is globally unique. - -``{event_title}`` [`My Conference`] - - Full title of the event. - -``{eventuser_id}`` [`e18u231`] - - A globally unique identifier for both the event and user. - -``{registration_title}`` [`Early Bird`] - - The title of the registration, as shown by Indico. - -Placeholders use the `Format String Syntax`_ of Python. -For example, ``{event_title:.6}`` is replaced with the first six characters of the event title. - -Note that both fields taking placeholders have a maximum size. -Since a template cannot be validated exactly, size validation assumes a reasonably terse input. -In practice, fields may be silently shortened after formatting with long input. - -Placeholder Examples -^^^^^^^^^^^^^^^^^^^^ - -Below are some examples for use as **Order Description** and **Order Identifier**: - -===================================================== ==================================== -Format Template Example Output -===================================================== ==================================== - **Order Description** ------------------------------------------------------------------------------------------- -``{event_title} (RegNr. {user_id})`` My Conference (RegNr. 231) -``{event_title}: {user_name} ({registration_title})`` My Conference: Jane Doe (Early Bird) -``{event_title} ({registration_title})`` My Conference (Early Bird) ------------------------------------------------------ ------------------------------------ - **Order Identifier** ------------------------------------------------------------------------------------------- -``{eventuser_id}-{user_firstname:.1}{user_lastname}`` e18u231-JDoe -``{event_title:.7} {eventuser_id}`` My Conf e18u231 -===================================================== ==================================== - - -.. _issue ticket: https://github.com/maxfischer2781/indico_sixpay/pulls - -.. _Format String Syntax: https://docs.python.org/3/library/string.html#formatstrings diff --git a/payment_sixpay/docs/source/design.rst b/payment_sixpay/docs/source/design.rst deleted file mode 100644 index 7d079a3..0000000 --- a/payment_sixpay/docs/source/design.rst +++ /dev/null @@ -1,24 +0,0 @@ -Implementation Overview -======================= - -The plugin follows the *Six Payment Services Payment Page* -`Specification Version 5.1 `_. -It implements the *Saferpay https interface* (Section 4 of the Specification), -but has to tie it into the Indico transaction flow. - -================= ================================================================= =============================== -HTTP API Plugin Component Implementation -================= ================================================================= =============================== -CreatePayInit :py:class:`~indico_sixpay.plugin.SixpayPaymentPlugin` ``_get_transaction_parameters`` - ``_get_payment_url`` -VerifyPayConfirm :py:class:`~indico_sixpay.request_handlers.SixPayResponseHandler` ``_verify_signature`` -CreatePayComplete :py:class:`~indico_sixpay.request_handlers.SixPayResponseHandler` ``_confirm_transaction`` -================= ================================================================= =============================== - -Payment Procedure ------------------ - -See below for an overview of the payment procedure: - -.. image:: images/uml/transaction.svg - :alt: UML of transaction diff --git a/payment_sixpay/docs/source/images/uml/transaction.svg b/payment_sixpay/docs/source/images/uml/transaction.svg deleted file mode 100644 index e4c1f13..0000000 --- a/payment_sixpay/docs/source/images/uml/transaction.svg +++ /dev/null @@ -1,58 +0,0 @@ -Indico ServiceSaferpayUserUserIndico ServerIndico ServerSixPay PluginSixPay PluginSixPaySixPaySaferpayPayment Requestrequest paymentpayment detailsSaferpaysaferpay urlpayment pagePayment Transactionsaferpay urlresultvalidate resultconfirmationvalidate detailsconfirm paymentalt[success]success[failure]failure[cancel]cancel \ No newline at end of file diff --git a/payment_sixpay/docs/source/installation.rst b/payment_sixpay/docs/source/installation.rst deleted file mode 100644 index f3a79fb..0000000 --- a/payment_sixpay/docs/source/installation.rst +++ /dev/null @@ -1,59 +0,0 @@ -Installation -============ - -The plugin can be installed using standard Python package managers. -To enable the plugin, it must be added to the configuration file of indico. - -.. seealso:: The official `Indico Plugin Installation Guide`_. - -Note that at least ``indico`` 2.0 is required, and will be installed automatically if it is missing. - -Installing the package ----------------------- - -The ``indico_sixpay`` plugin must be installed for the python version running ``indico``. -With a standard indico installation, you must activate the indico python virtual environment first. - -.. code:: bash - - su - indico - source ~/.venv/bin/activate - -The latest release version is available for the default python package managers. -You can directly install the module using ``pip``: - -.. code:: bash - - pip install indico_sixpay - -This can also be used to upgrade to a newer version: - -.. code:: bash - - pip install indico_sixpay --upgrade - -Enabling the package --------------------- - -All plugins must be enabled in indico's configuration file. -By default, the configuration is located in ``/opt/indico/etc/indico.conf``. - -.. code:: python - - PLUGINS = {'payment_sixpay'} - -Note that if you need multiple plugins, you must all include them in the set of ``PLUGINS``: - -.. code:: python - - PLUGINS = {'payment_manual', 'payment_paypal', 'payment_sixpay'} - -After changing the configuration, trigger a reload of the indico services. -Issue the following commands as ``root``: - -.. code:: bash - - touch ~/web/indico.wsgi - systemctl restart indico-celery.service - -.. _Indico Plugin Installation Guide: https://docs.getindico.io/en/latest/installation/plugins/ diff --git a/payment_sixpay/docs/source/uml/transaction.uml b/payment_sixpay/docs/source/uml/transaction.uml deleted file mode 100644 index cff90a0..0000000 --- a/payment_sixpay/docs/source/uml/transaction.uml +++ /dev/null @@ -1,45 +0,0 @@ -@startuml -actor User - -box "Indico Service" - participant "Indico Server" as Indico - participant "SixPay Plugin" as Plugin -end box - -== Payment Request== - User -> Indico: request payment - activate Indico - Indico -> Plugin - activate Plugin - Plugin -> SixPay: payment details - activate SixPay - create participant Saferpay - SixPay -> Saferpay - Plugin <-- SixPay: saferpay url - deactivate SixPay - Indico <- Plugin - deactivate Plugin - User <- Indico: payment page - deactivate Indico -... - -== Payment Transaction == -User -> Saferpay: saferpay url -activate Saferpay - Plugin <<- Saferpay: result - activate Plugin - Plugin -> SixPay: validate result - Plugin <- SixPay: confirmation - Plugin -> Plugin: validate details - Indico <- Plugin: confirm payment - deactivate Plugin - User <- Saferpay -destroy Saferpay -alt success - User -> Plugin: success -else failure - User -> Plugin: failure -else cancel - User -> Plugin: cancel -end -@enduml From fcec6ef081d38f0fb927b83f4bd26803281d6b4a Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 14:50:25 +0200 Subject: [PATCH 11/14] Re-license under MIT license Signed-off-by: Max Fischer Signed-off-by: Martin Claus Signed-off-by: Claas Faber --- payment_sixpay/LICENSE | 695 +----------------- payment_sixpay/headers.yml | 11 + payment_sixpay/indico_sixpay/__about__.py | 7 + payment_sixpay/indico_sixpay/__init__.py | 20 +- payment_sixpay/indico_sixpay/blueprint.py | 21 +- payment_sixpay/indico_sixpay/plugin.py | 20 +- .../indico_sixpay/request_handlers.py | 21 +- payment_sixpay/indico_sixpay/utility.py | 21 +- payment_sixpay/setup.py | 10 +- 9 files changed, 72 insertions(+), 754 deletions(-) create mode 100644 payment_sixpay/headers.yml diff --git a/payment_sixpay/LICENSE b/payment_sixpay/LICENSE index 94a9ed0..c4a98ae 100644 --- a/payment_sixpay/LICENSE +++ b/payment_sixpay/LICENSE @@ -1,674 +1,21 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) European Organization for Nuclear Research (CERN) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/payment_sixpay/headers.yml b/payment_sixpay/headers.yml new file mode 100644 index 0000000..0b33f8f --- /dev/null +++ b/payment_sixpay/headers.yml @@ -0,0 +1,11 @@ +root: true +name: Max Fischer, Martin Claus, CERN +start_year: 2017 +header: |- + {comment_start} This file is part of the Indico plugins. + {comment_middle} Copyright (C) {dates} {name} + {comment_middle} + {comment_middle} The Indico plugins are free software; you can redistribute + {comment_middle} them and/or modify them under the terms of the MIT License; + {comment_middle} see the LICENSE file for more details. + {comment_end} diff --git a/payment_sixpay/indico_sixpay/__about__.py b/payment_sixpay/indico_sixpay/__about__.py index 424da96..ba7f025 100644 --- a/payment_sixpay/indico_sixpay/__about__.py +++ b/payment_sixpay/indico_sixpay/__about__.py @@ -1,3 +1,10 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + """ ++++++++++++++++++++++++++++++++++++++++++++++++++ ``indico_sixpay`` - SIX EPayment Plugin for Indico diff --git a/payment_sixpay/indico_sixpay/__init__.py b/payment_sixpay/indico_sixpay/__init__.py index 64a76f2..aaa0ef4 100644 --- a/payment_sixpay/indico_sixpay/__init__.py +++ b/payment_sixpay/indico_sixpay/__init__.py @@ -1,16 +1,6 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN # -# This file is part of the SixPay Indico EPayment Plugin. -# Copyright (C) 2017 - 2018 Max Fischer -# -# This is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SixPay Indico EPayment Plugin;if not, see . +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py index b24f55d..bc724a3 100644 --- a/payment_sixpay/indico_sixpay/blueprint.py +++ b/payment_sixpay/indico_sixpay/blueprint.py @@ -1,20 +1,9 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN # -# This file is part of the SixPay Indico EPayment Plugin. -# Copyright (C) 2017 - 2018 Max Fischer -# -# This is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SixPay Indico EPayment Plugin; -# if not, see . +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. """Definition of callbacks exposed by the Indico server.""" from indico.core.plugins import IndicoPluginBlueprint diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py index ed14ad5..2aec664 100644 --- a/payment_sixpay/indico_sixpay/plugin.py +++ b/payment_sixpay/indico_sixpay/plugin.py @@ -1,19 +1,9 @@ -# This file is part of the SixPay Indico EPayment Plugin. -# Copyright (C) 2017 - 2018 Max Fischer +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN # -# This is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SixPay Indico EPayment Plugin; -# if not, see . +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. """ Core of the SixPay plugin. diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py index d0585d7..2682c34 100644 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ b/payment_sixpay/indico_sixpay/request_handlers.py @@ -1,20 +1,9 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN # -# This file is part of the SixPay Indico EPayment Plugin. -# Copyright (C) 2017 - 2018 Max Fischer -# -# This is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SixPay Indico EPayment Plugin; -# if not, see . +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. """Callbacks for asynchronous replies by Saferpay and to redirect the user.""" import requests diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_sixpay/utility.py index 613630c..253bea0 100644 --- a/payment_sixpay/indico_sixpay/utility.py +++ b/payment_sixpay/indico_sixpay/utility.py @@ -1,20 +1,9 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN # -# This file is part of the SixPay Indico EPayment Plugin. -# Copyright (C) 2017 - 2018 Max Fischer -# -# This is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 3 of the -# License, or (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with SixPay Indico EPayment Plugin; -# if not, see . +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. """Utility functions used by the Sixpay payment plugin.""" import uuid diff --git a/payment_sixpay/setup.py b/payment_sixpay/setup.py index f662d15..071a172 100644 --- a/payment_sixpay/setup.py +++ b/payment_sixpay/setup.py @@ -1,3 +1,9 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. import os from setuptools import find_packages, setup @@ -26,7 +32,7 @@ setup( package_data={'indico_sixpay': ['templates/*.html']}, install_requires=['requests', 'indico>=3.0', 'iso4217'], python_requires='~=3.9.0', - license='GPLv3+', + license='MIT', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Plugins', @@ -34,7 +40,7 @@ setup( 'Intended Audience :: Science/Research', 'Intended Audience :: System Administrators', 'Intended Audience :: Other Audience', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Topic :: Communications :: Conferencing', From 2565ad5f786e160bf071604e662f8055d58ca1a7 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Wed, 28 Jul 2021 19:01:27 +0200 Subject: [PATCH 12/14] Cleanup, fix issues, and use Indico code style --- payment_sixpay/MANIFEST.in | 4 + payment_sixpay/README.md | 58 +-- .../__init__.py | 5 + .../indico_payment_sixpay/blueprint.py | 22 + .../indico_payment_sixpay/controllers.py | 264 ++++++++++++ payment_sixpay/indico_payment_sixpay/forms.py | 168 ++++++++ .../indico_payment_sixpay/plugin.py | 163 ++++++++ .../templates/event_payment_form.html | 6 +- .../templates/transaction_details.html | 7 + .../util.py} | 51 +-- payment_sixpay/indico_sixpay/__about__.py | 61 --- payment_sixpay/indico_sixpay/blueprint.py | 32 -- payment_sixpay/indico_sixpay/plugin.py | 391 ------------------ .../indico_sixpay/request_handlers.py | 329 --------------- payment_sixpay/setup.cfg | 33 ++ payment_sixpay/setup.py | 44 +- 16 files changed, 694 insertions(+), 944 deletions(-) create mode 100644 payment_sixpay/MANIFEST.in rename payment_sixpay/{indico_sixpay => indico_payment_sixpay}/__init__.py (74%) create mode 100644 payment_sixpay/indico_payment_sixpay/blueprint.py create mode 100644 payment_sixpay/indico_payment_sixpay/controllers.py create mode 100644 payment_sixpay/indico_payment_sixpay/forms.py create mode 100644 payment_sixpay/indico_payment_sixpay/plugin.py rename payment_sixpay/{indico_sixpay => indico_payment_sixpay}/templates/event_payment_form.html (75%) create mode 100644 payment_sixpay/indico_payment_sixpay/templates/transaction_details.html rename payment_sixpay/{indico_sixpay/utility.py => indico_payment_sixpay/util.py} (66%) delete mode 100644 payment_sixpay/indico_sixpay/__about__.py delete mode 100644 payment_sixpay/indico_sixpay/blueprint.py delete mode 100644 payment_sixpay/indico_sixpay/plugin.py delete mode 100644 payment_sixpay/indico_sixpay/request_handlers.py create mode 100644 payment_sixpay/setup.cfg diff --git a/payment_sixpay/MANIFEST.in b/payment_sixpay/MANIFEST.in new file mode 100644 index 0000000..a45de03 --- /dev/null +++ b/payment_sixpay/MANIFEST.in @@ -0,0 +1,4 @@ +graft indico_payment_sixpay/templates +graft indico_payment_sixpay/translations + +global-exclude *.pyc __pycache__ .keep diff --git a/payment_sixpay/README.md b/payment_sixpay/README.md index 78377b3..e84a95a 100644 --- a/payment_sixpay/README.md +++ b/payment_sixpay/README.md @@ -1,54 +1,18 @@ -# Indico 2 EPayment plugin for SIX Payment Services +# SIXPay Payment Plugin -Plugin for the Indico 2 event/conference management system, enabling support for SIX Payment Service. -This enables EPayment for users via the SixPay Saferpay Payment Page in conferences and other events. +This plugin provides a SIXPay payment option for Indico's payment module. -Please see the [documentation](http://indico-sixpay.readthedocs.io/en/latest/) for details on installation, usage and maintenance. +When used, the user will be sent to SIXPay to make the payment, and afterwards +they are automatically sent back to Indico. -## Overview +## Changelog -If the plugin is enabled, event participants can select the ``SixPay`` payment method during the EPayment checkout. -Payment is performed via the **Saferpay Payment Page**, an external service provided by SIX Payment Services. -The plugin handles the user interaction inside Indico, and the secure, asynchronous transaction with SIX Payment Services. +### 3.0 -*This is plugin supports Indico 2.0.* -*The legacy plugin for Indico 1.2 is [hosted on github](https://github.com/maxfischer2781/indico_sixpay/tree/indico-1.2).* +- Initial release for Indico 3.0 -## Installation +## Credits -The plugin can be installed using standard Python package managers. -Note that at least `indico` 2.0 is required, and will be installed if it is missing. - -**Note**: The `indico_sixpay` plugin must be installed for the python version running `indico`. - -### Release Version - -The latest release version is available for the default python package managers. -You can directly install the module using `pip`: - - pip install indico_sixpay - -This can also be used to upgrade to a newer version: - - pip install indico_sixpay --upgrade - -### Latest Version - -Download this repository to any host running indico. -Install it by running: - - python setup.py install - -After reloading the EPayment plugin in the Indico Admin panel, you can enable the SixPay service. - -## Contributing, Feedback and Bug Reports - -This project is hosted on [github](https://github.com/maxfischer2781/indico_sixpay). -If you encounter any bugs or missing features, please use the [bug tracker](https://github.com/maxfischer2781/indico_sixpay/issues) or submit a [pull request](https://github.com/maxfischer2781/indico_sixpay/pulls). - -## Disclaimer - -This plugin is in no way endorsed, supported or provided by SIX, Indico, KIT or any other service, provider or entity. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Originally developed by Max Fischer for Indico 1.2 and 2.x. Updated to use the +latest SIXPay API by Martin Claus. Adapted to Indico 3.0 and Python 3 by the +CERN Indico Team. diff --git a/payment_sixpay/indico_sixpay/__init__.py b/payment_sixpay/indico_payment_sixpay/__init__.py similarity index 74% rename from payment_sixpay/indico_sixpay/__init__.py rename to payment_sixpay/indico_payment_sixpay/__init__.py index aaa0ef4..7dda1a5 100644 --- a/payment_sixpay/indico_sixpay/__init__.py +++ b/payment_sixpay/indico_payment_sixpay/__init__.py @@ -4,3 +4,8 @@ # The Indico plugins are free software; you can redistribute # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. + +from indico.util.i18n import make_bound_gettext + + +_ = make_bound_gettext('payment_sixpay') diff --git a/payment_sixpay/indico_payment_sixpay/blueprint.py b/payment_sixpay/indico_payment_sixpay/blueprint.py new file mode 100644 index 0000000..e12a9a0 --- /dev/null +++ b/payment_sixpay/indico_payment_sixpay/blueprint.py @@ -0,0 +1,22 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + +from indico.core.plugins import IndicoPluginBlueprint + +from indico_payment_sixpay.controllers import (SixPayNotificationHandler, UserCancelHandler, UserFailureHandler, + UserSuccessHandler) + + +blueprint = IndicoPluginBlueprint( + 'payment_sixpay', __name__, + url_prefix='/event//registrations//payment/response/sixpay' +) + +blueprint.add_url_rule('/failure', 'failure', UserCancelHandler, methods=('GET', 'POST')) +blueprint.add_url_rule('/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST')) +blueprint.add_url_rule('/success', 'success', UserSuccessHandler, methods=('GET', 'POST')) +blueprint.add_url_rule('/ipn', 'notify', SixPayNotificationHandler, methods=('Get', 'POST')) diff --git a/payment_sixpay/indico_payment_sixpay/controllers.py b/payment_sixpay/indico_payment_sixpay/controllers.py new file mode 100644 index 0000000..168641e --- /dev/null +++ b/payment_sixpay/indico_payment_sixpay/controllers.py @@ -0,0 +1,264 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + +from urllib.parse import urljoin + +import requests +from flask import flash, redirect, request +from werkzeug.exceptions import BadRequest + +from indico.modules.events.payment.models.transactions import TransactionAction +from indico.modules.events.payment.notifications import notify_amount_inconsistency +from indico.modules.events.payment.util import register_transaction +from indico.modules.events.registration.models.registrations import Registration +from indico.web.flask.util import url_for +from indico.web.rh import RH + +from indico_payment_sixpay import _ +from indico_payment_sixpay.plugin import SixpayPaymentPlugin +from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, SIXPAY_PP_ASSERT_URL, + SIXPAY_PP_CANCEL_URL, SIXPAY_PP_CAPTURE_URL, get_request_header, get_setting, + to_large_currency, to_small_currency) + + +class TransactionFailure(Exception): + """A transaction with SixPay failed. + + :param step: name of the step at which the transaction failed + :param details: verbose description of what went wrong + """ + + def __init__(self, step, details=None): + self.step = step + self.details = details + + +class RHSixpayBase(RH): + """Request Handler for asynchronous callbacks from SixPay. + + These handlers are used either by + + - the user, when he is redirected from SixPay back to Indico + - SixPay, when it sends back the result of a transaction + """ + + CSRF_ENABLED = False + + def _process_args(self): + self.registration = Registration.query.filter_by(uuid=request.args['token']).first() + if not self.registration: + raise BadRequest + self.token = self.registration.transaction.data['Init_PP_response']['Token'] + + def _get_setting(self, setting): + return get_setting(setting, self.registration.event) + + +class SixPayNotificationHandler(RHSixpayBase): + """Handler for notification from SixPay service.""" + + def __init__(self): + """Initialize request handler.""" + super().__init__() + self.sixpay_url = None + + def _process_args(self): + super()._process_args() + self.sixpay_url = get_setting('url') + + def _process(self): + """Process the reply from SixPay about the transaction.""" + try: + self._process_confirmation() + except TransactionFailure as exc: + SixpayPaymentPlugin.logger.warning('SixPay transaction failed during %s: %s', exc.step, exc.details) + + def _process_confirmation(self): + """Process the confirmation response inside indico.""" + # assert transaction status from SixPay + try: + assert_response = self._assert_payment() + if self._is_duplicate_transaction(assert_response): + # we have already handled the transaction + return True + if self._is_authorized(assert_response) and not self._is_captured(assert_response): + self._capture_transaction(assert_response) + self._verify_amount(assert_response) + self._register_payment(assert_response) + except TransactionFailure as exc: + SixpayPaymentPlugin.logger.warning('SixPay transaction failed during %s: %s', exc.step, exc.details) + raise + return True + + def _perform_request(self, task, endpoint, data): + """Perform a request against SixPay. + + :param task: description of the request, used for error handling + :param endpoint: the URL endpoint *relative* to the SixPay base URL + :param **data: data passed during the request + + This will automatically raise any HTTP errors encountered during the + request. If the request itself fails, a :py:exc:`~.TransactionFailure` + is raised for ``task``. + """ + request_url = urljoin(self.sixpay_url, endpoint) + credentials = (get_setting('username'), get_setting('password')) + response = requests.post(request_url, json=data, auth=credentials) + try: + response.raise_for_status() + except requests.HTTPError: + raise TransactionFailure(step=task, details=response.text) + return response + + def _assert_payment(self): + """Check the status of the transaction with SixPay. + + Returns transaction assert data. + """ + assert_response = self._perform_request( + 'assert', + SIXPAY_PP_ASSERT_URL, + { + 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, self._get_setting('account_id')), + 'Token': self.token, + } + ) + if assert_response.ok: + return assert_response.json() + + def _is_duplicate_transaction(self, transaction_data): + """Check if this transaction has already been recorded.""" + prev_transaction = self.registration.transaction + if ( + not prev_transaction or + prev_transaction.provider != PROVIDER_SIXPAY or + 'Transaction' not in prev_transaction.data + ): + return False + old = prev_transaction.data.get('Transaction') + new = transaction_data.get('Transaction') + return ( + old['OrderId'] == new['OrderId'] and + old['Type'] == new['Type'] and + old['Id'] == new['Id'] and + old['SixTransactionReference'] == new['SixTransactionReference'] and + old['Amount']['Value'] == new['Amount']['Value'] and + old['Amount']['CurrencyCode'] == new['Amount']['CurrencyCode'] + ) + + def _is_authorized(self, assert_data): + """Check if payment is authorized.""" + return assert_data['Transaction']['Status'] == 'AUTHORIZED' + + def _is_captured(self, assert_data): + """Check if payment is captured, i.e. the cash flow is triggered.""" + return assert_data['Transaction']['Status'] == 'CAPTURED' + + def _verify_amount(self, assert_data): + """Verify the amount and currency of the payment. + + Sends an email but still registers incorrect payments. + """ + expected_amount = float(self.registration.price) + expected_currency = self.registration.currency + amount = float(assert_data['Transaction']['Amount']['Value']) + currency = assert_data['Transaction']['Amount']['CurrencyCode'] + if to_small_currency(expected_amount, expected_currency) == amount and expected_currency == currency: + return True + SixpayPaymentPlugin.logger.warning("Payment doesn't match event's fee: %s %s != %s %s", + amount, currency, to_small_currency(expected_amount, expected_currency), + expected_currency) + notify_amount_inconsistency(self.registration, to_large_currency(amount, currency), currency) + return False + + def _capture_transaction(self, assert_data): + """Confirm to SixPay that the transaction is accepted. + + On success returns the response JSON data. + """ + capture_data = { + 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, self._get_setting('account_id')), + 'TransactionReference': {'TransactionId': assert_data['Transaction']['Id']} + } + capture_response = self._perform_request('capture', SIXPAY_PP_CAPTURE_URL, capture_data) + return capture_response.json() + + def _cancel_transaction(self, assert_data): + """Inform Sixpay that the transaction is canceled. + + Cancel the transaction at Sixpay. This method is implemented but + not used and tested yet. + """ + cancel_data = { + 'RequestHeader': get_request_header( + SIXPAY_JSON_API_SPEC, self._get_setting('account_id') + ), + 'TransactionReference': { + 'TransactionId': assert_data['Transaction']['Id'] + } + } + cancel_response = self._perform_request( + 'cancel', SIXPAY_PP_CANCEL_URL, cancel_data + ) + return cancel_response.json() + + def _register_payment(self, assert_data): + """Register the transaction as paid.""" + register_transaction( + self.registration, + self.registration.transaction.amount, + self.registration.transaction.currency, + TransactionAction.complete, + PROVIDER_SIXPAY, + data={'Transaction': assert_data['Transaction']} + ) + + +class UserCancelHandler(RHSixpayBase): + """User redirect target in case of cancelled payment.""" + + def _process(self): + register_transaction( + self.registration, + self.registration.transaction.amount, + self.registration.transaction.currency, + # XXX: this is indeed reject and not cancel (cancel is "mark as unpaid" and + # only used for manual transactions) + TransactionAction.reject, + provider=PROVIDER_SIXPAY, + ) + flash(_('You cancelled the payment.'), 'info') + return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + + +class UserFailureHandler(RHSixpayBase): + """User redirect target in case of failed payment.""" + + def _process(self): + register_transaction( + self.registration, + self.registration.transaction.amount, + self.registration.transaction.currency, + TransactionAction.reject, + provider=PROVIDER_SIXPAY, + ) + flash(_('Your payment has failed.'), 'info') + return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) + + +class UserSuccessHandler(SixPayNotificationHandler): + """User redirect target in case of successful payment.""" + + def _process(self): + try: + self._process_confirmation() + except TransactionFailure as exc: + SixpayPaymentPlugin.logger.warning('SixPay transaction failed during %s: %s', exc.step, exc.details) + flash(_('Your payment could not be confirmed. Please contact the event organizers.'), 'warning') + else: + flash(_('Your payment has been confirmed.'), 'success') + return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) diff --git a/payment_sixpay/indico_payment_sixpay/forms.py b/payment_sixpay/indico_payment_sixpay/forms.py new file mode 100644 index 0000000..293198d --- /dev/null +++ b/payment_sixpay/indico_payment_sixpay/forms.py @@ -0,0 +1,168 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + +from wtforms.fields import StringField +from wtforms.fields.html5 import URLField +from wtforms.validators import DataRequired, Email, Length, Optional, ValidationError + +from indico.modules.events.payment import PaymentEventSettingsFormBase, PaymentPluginSettingsFormBase +from indico.web.forms.fields import IndicoPasswordField +from indico.web.forms.validators import IndicoRegexp + +from indico_payment_sixpay import _ + + +# XXX: Maybe this could be refactored to use the standard indico Placeholder system? +class FormatField: + """Validator for format fields, i.e. strings with ``{key}`` placeholders. + + :param max_length: optional maximum length, checked on a test formatting + :param field_map: keyword arguments to use for test formatting + + On validation, a test mapping is applied to the field. This ensures the + field has a valid ``str.format`` format, and does not use illegal keys + (as determined by ``default_field_map`` and ``field_map``). + The ``max_length`` is validated against the test-formatted field, which + is an estimate for an average sized input. + """ + + #: default placeholders to test length after formatting + default_field_map = { + 'user_id': 12345, + 'user_name': 'Jane Whiteacre', + 'user_firstname': 'Jane', + 'user_lastname': 'Whiteacre', + 'event_id': 12345, + 'event_title': 'Placeholder: The Event', + 'registration_id': 12345, + 'regform_title': 'EarlyBird Registration' + } + + def __init__(self, max_length=float('inf'), field_map=None): + """Format field validator, i.e. strings with ``{key}`` placeholders. + + :param max_length: optional maximum length, + checked on a test formatting + :param field_map: keyword arguments to use for test formatting + """ + self.max_length = max_length + self.field_map = self.default_field_map.copy() + if field_map is not None: + self.field_map.update(field_map) + + def __call__(self, form, field): + """Validate format field data. + + Returns true on successful validation, else an ValidationError is + thrown. + """ + if not field.data: + return True + try: + test_format = field.data.format(**self.field_map) + except KeyError as exc: + raise ValidationError(_('Invalid format string key: {}').format(exc)) + except ValueError as exc: + raise ValidationError(_('Malformed format string: {}').format(exc)) + if len(test_format) > self.max_length: + raise ValidationError( + _('Format string too long: shortest replacement with {len}, expected {max}') + .format(len=len(test_format), max=self.max_length) + ) + return True + + +class PluginSettingsForm(PaymentPluginSettingsFormBase): + """Configuration form for the Plugin across all events.""" + + url = URLField( + label=_('Saferpay JSON API URL'), + validators=[DataRequired()], + description=_('URL to contact the Saferpay JSON API'), + ) + username = StringField( + label=_('API username'), + validators=[DataRequired()], + description=_('The username to access the SaferPay JSON API') + ) + password = IndicoPasswordField( + label=_('API password'), + validators=[DataRequired()], + description=_('The password to access the SaferPay JSON API'), + toggle=True, + ) + account_id = StringField( + label=_('Account ID'), + validators=[ + Optional(), + IndicoRegexp(r'^[0-9-]{0,15}$') + ], + description=_( + 'Default Saferpay account ID, such as "123456-12345678". ' + 'Event managers will be able to override this.' + ) + ) + order_description = StringField( + label=_('Order Description'), + validators=[DataRequired(), FormatField(max_length=80)], + description=_( + 'The default description of each order in a human readable way. ' + 'It is presented to the registrant during the transaction with Saferpay. ' + 'Event managers will be able to override this.' + ) + ) + order_identifier = StringField( + label=_('Order Identifier'), + validators=[DataRequired(), FormatField(max_length=80)], + description=_( + 'The default identifier of each order for further processing. ' + 'Event managers will be able to override this.' + ) + ) + notification_mail = StringField( + label=_('Notification Email'), + validators=[Optional(), Email(), Length(0, 50)], + description=_( + 'Emmil address to receive notifications of transactions. ' + "This is independent of Indico's own payment notifications. " + 'Event managers will be able to override this.' + ) + ) + + +class EventSettingsForm(PaymentEventSettingsFormBase): + """Configuration form for the plugin for a specific event.""" + + account_id = StringField( + label=_('Account ID'), + validators=[ + DataRequired(), + IndicoRegexp(r'^[0-9-]{0,15}$') + ], + description=_('The Saferpay account ID, such as "123456-12345678".') + ) + order_description = StringField( + label=_('Order Description'), + validators=[DataRequired(), FormatField(max_length=80)], + description=_( + 'The description of each order in a human readable way. ' + 'It is presented to the registrant during the transaction with Saferpay.' + ) + ) + order_identifier = StringField( + label=_('Order Identifier'), + validators=[DataRequired(), FormatField(max_length=80)], + description=_('The default identifier of each order for further processing.') + ) + notification_mail = StringField( + label=_('Notification Email'), + validators=[DataRequired(), Email(), Length(0, 50)], + description=_( + 'Emmil address to receive notifications of transactions. ' + "This is independent of Indico's own payment notifications." + ) + ) diff --git a/payment_sixpay/indico_payment_sixpay/plugin.py b/payment_sixpay/indico_payment_sixpay/plugin.py new file mode 100644 index 0000000..959ba4b --- /dev/null +++ b/payment_sixpay/indico_payment_sixpay/plugin.py @@ -0,0 +1,163 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + +from urllib.parse import urljoin + +import requests +from requests import RequestException + +from indico.core.plugins import IndicoPlugin, url_for_plugin +from indico.modules.events.payment import PaymentPluginMixin +from indico.modules.events.payment.models.transactions import TransactionAction +from indico.modules.events.payment.util import register_transaction + +from indico_payment_sixpay.forms import EventSettingsForm, PluginSettingsForm +from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, SIXPAY_PP_INIT_URL, get_request_header, + get_terminal_id, to_small_currency) + + +class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): + """SIXPay + + Provides a payment method using the SIXPay Saferpay API. + """ + + configurable = True + #: form for default configuration across events + settings_form = PluginSettingsForm + #: form for configuration for specific events + event_settings_form = EventSettingsForm + #: global default settings - should be a reasonable default + default_settings = { + 'method_name': 'SIXPay', + 'url': 'https://www.saferpay.com/api/', + 'username': None, + 'password': None, + 'account_id': None, + 'order_description': '{event_title}, {regform_title}, {user_name}', + 'order_identifier': 'e{event_id}r{registration_id}', + 'notification_mail': None + } + #: per event default settings - use the global settings + default_event_settings = { + 'enabled': False, + 'method_name': None, + 'account_id': None, + 'order_description': None, + 'order_identifier': None, + 'notification_mail': None, + } + + def get_blueprints(self): + """Blueprint for URL endpoints with callbacks.""" + from indico_payment_sixpay.blueprint import blueprint + return blueprint + + # Dear future Maintainer, + # - the business logic is here! + # - see PaymentPluginMixin.render_payment_form for what `data` provides + # - What happens here + # - We add `success`, `cancel` and `failure` for *sixpay* to redirect the + # user back to us AFTER his request + # - We add `notify` for *sixpay* to inform us asynchronously about + # the result + # - We send a request to initialize the pyment page to SixPay to get a + # request url for this transaction + # - We put the payment page URL and token we got into `data` + # - Return uses `indico_payment_sixpay/templates/event_payment_form.html`, + # presenting a trigger button to the user + def adjust_payment_form_data(self, data): + """Prepare the payment form shown to registrants.""" + global_settings = data['settings'] + transaction = self._get_transaction_parameters(data) + init_response = self._init_payment_page( + sixpay_url=global_settings['url'], + transaction_data=transaction, + credentials=(global_settings['username'], global_settings['password']) + ) + data['payment_url'] = init_response['RedirectUrl'] + + # create pending transaction and store Saferpay transaction token + new_indico_txn = register_transaction( + data['registration'], + data['amount'], + data['currency'], + TransactionAction.pending, + PROVIDER_SIXPAY, + {'Init_PP_response': init_response} + ) + if not new_indico_txn: + # set it on the current transaction if we could not create a next one + # this happens if we already have a pending transaction and it's incredibly + # ugly... + data['registration'].transaction.data = {'Init_PP_response': init_response} + return data + + @staticmethod + def get_field_format_map(registration): + """Generate dict which provides registration information.""" + return { + 'user_id': registration.user_id, + 'user_name': registration.full_name, + 'user_firstname': registration.first_name, + 'user_lastname': registration.last_name, + 'event_id': registration.event_id, + 'event_title': registration.event.title, + 'registration_id': registration.id, + 'regform_title': registration.registration_form.title + } + + def _get_transaction_parameters(self, payment_data): + """Parameters for formulating a transaction request.""" + settings = payment_data['event_settings'] + registration = payment_data['registration'] + format_map = self.get_field_format_map(registration) + for format_field in ('order_description', 'order_identifier'): + payment_data[format_field] = settings[format_field].format(**format_map) + + # see the SixPay Manual + # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize + # on what these things mean + transaction_parameters = { + 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, settings['account_id']), + 'TerminalId': get_terminal_id(settings['account_id']), + 'Payment': { + 'Amount': { + # indico handles price as largest currency, but six expects + # smallest. E.g. EUR: indico uses 100.2 Euro, but six + # expects 10020 Cent + 'Value': str(to_small_currency(payment_data['amount'], payment_data['currency'])), + 'CurrencyCode': payment_data['currency'], + }, + 'OrderId': payment_data['order_identifier'][:80], + 'DESCRIPTION': payment_data['order_description'][:1000], + }, + # callbacks of the transaction - where to announce success etc., when redircting the user + 'ReturnUrls': { + 'Success': url_for_plugin('payment_sixpay.success', registration.locator.uuid, _external=True), + 'Fail': url_for_plugin('payment_sixpay.failure', registration.locator.uuid, _external=True), + 'Abort': url_for_plugin('payment_sixpay.cancel', registration.locator.uuid, _external=True) + }, + 'Notification': { + # where to asynchronously call back from SixPay + 'NotifyUrl': url_for_plugin('payment_sixpay.notify', registration.locator.uuid, _external=True) + } + } + if settings['notification_mail']: + transaction_parameters['Notification']['MerchantEmails'] = [settings['notification_mail']] + return transaction_parameters + + def _init_payment_page(self, sixpay_url, transaction_data, credentials): + """Initialize payment page.""" + endpoint = urljoin(sixpay_url, SIXPAY_PP_INIT_URL) + resp = requests.post(endpoint, json=transaction_data, auth=credentials) + try: + resp.raise_for_status() + except RequestException as exc: + self.logger.error('Could not initialize payment: %s', exc.response.text) + raise Exception('Could not initialize payment') + return resp.json() diff --git a/payment_sixpay/indico_sixpay/templates/event_payment_form.html b/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html similarity index 75% rename from payment_sixpay/indico_sixpay/templates/event_payment_form.html rename to payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html index 36f7342..e6d6afb 100644 --- a/payment_sixpay/indico_sixpay/templates/event_payment_form.html +++ b/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html @@ -8,9 +8,5 @@ Clicking on the {% trans %}Pay Now{% endtrans %} button you wil
{% trans %}Total amount{% endtrans %}
{{ format_currency(amount, currency, locale=session.lang) }}
-
-
- -
-
+
{% trans %}Pay Now{% endtrans %}
diff --git a/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html b/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html new file mode 100644 index 0000000..a24f431 --- /dev/null +++ b/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html @@ -0,0 +1,7 @@ +{% extends 'events/payment/transaction_details.html' %} +{% block details %} + {% if transaction.data.Transaction %} +
{% trans %}Order ID{% endtrans %}
+
{{ transaction.data.Transaction.OrderId }}
+ {% endif %} +{% endblock %} diff --git a/payment_sixpay/indico_sixpay/utility.py b/payment_sixpay/indico_payment_sixpay/util.py similarity index 66% rename from payment_sixpay/indico_sixpay/utility.py rename to payment_sixpay/indico_payment_sixpay/util.py index 253bea0..85b3827 100644 --- a/payment_sixpay/indico_sixpay/utility.py +++ b/payment_sixpay/indico_payment_sixpay/util.py @@ -4,29 +4,24 @@ # The Indico plugins are free software; you can redistribute # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. -"""Utility functions used by the Sixpay payment plugin.""" import uuid import iso4217 -from flask_pluginengine import current_plugin from werkzeug.exceptions import NotImplemented as HTTPNotImplemented -from indico.util.i18n import make_bound_gettext +from indico_payment_sixpay import _ -#: internationalisation/localisation of strings -gettext = make_bound_gettext('payment_sixpay') - # Saferpay API details -saferpay_json_api_spec = '1.12' -saferpay_pp_init_url = 'Payment/v1/PaymentPage/Initialize' -saferpay_pp_assert_url = 'Payment/v1/PaymentPage/Assert' -saferpay_pp_capture_url = 'Payment/v1/Transaction/Capture' -saferpay_pp_cancel_url = 'Payment/v1/Transaction/Cancel' +SIXPAY_JSON_API_SPEC = '1.12' +SIXPAY_PP_INIT_URL = 'Payment/v1/PaymentPage/Initialize' +SIXPAY_PP_ASSERT_URL = 'Payment/v1/PaymentPage/Assert' +SIXPAY_PP_CAPTURE_URL = 'Payment/v1/Transaction/Capture' +SIXPAY_PP_CANCEL_URL = 'Payment/v1/Transaction/Cancel' -# provider string -provider = 'sixpay' +# payment provider identifier +PROVIDER_SIXPAY = 'sixpay' # currencies for which the major to minor currency ratio # is not a multiple of 10 @@ -37,24 +32,17 @@ def validate_currency(iso_code): """Check whether the currency can be properly handled by this plugin. :param iso_code: an ISO4217 currency code, e.g. ``"EUR"`` - :type iso_code: basestring :raises: :py:exc:`~.HTTPNotImplemented` if the currency is not valid """ if iso_code in NON_DECIMAL_CURRENCY: raise HTTPNotImplemented( - gettext( - "Unsupported currency '{0}' for SixPay." - ' Please contact the organisers' - ).format(iso_code) + _("Unsupported currency '{}' for SixPay. Please contact the organizers").format(iso_code) ) try: iso4217.Currency(iso_code) except ValueError: raise HTTPNotImplemented( - gettext( - "Unknown currency '{0}' for SixPay." - ' Please contact the organisers' - ).format(iso_code) + _("Unknown currency '{}' for SixPay. Please contact the organizers").format(iso_code) ) @@ -82,21 +70,12 @@ def to_large_currency(small_currency_amount, iso_code): def get_request_header(api_spec, account_id): - """Return request header dict. - - Contained information: - - SpecVersion - - CustomerId - - RequestId - - RetryIndicator - """ - request_header = { + return { 'SpecVersion': api_spec, 'CustomerId': get_customer_id(account_id), 'RequestId': str(uuid.uuid4()), 'RetryIndicator': 0, } - return request_header def get_customer_id(account_id): @@ -118,10 +97,8 @@ def get_terminal_id(account_id): def get_setting(setting, event=None): """Return a configuration setting of the plugin.""" + from indico_payment_sixpay.plugin import SixpayPaymentPlugin if event: - return ( - current_plugin.event_settings.get(event, setting) - or current_plugin.settings.get(setting) - ) + return SixpayPaymentPlugin.event_settings.get(event, setting) else: - return current_plugin.settings.get(setting) + return SixpayPaymentPlugin.settings.get(setting) diff --git a/payment_sixpay/indico_sixpay/__about__.py b/payment_sixpay/indico_sixpay/__about__.py deleted file mode 100644 index ba7f025..0000000 --- a/payment_sixpay/indico_sixpay/__about__.py +++ /dev/null @@ -1,61 +0,0 @@ -# This file is part of the Indico plugins. -# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN -# -# The Indico plugins are free software; you can redistribute -# them and/or modify them under the terms of the MIT License; -# see the LICENSE file for more details. - -""" -++++++++++++++++++++++++++++++++++++++++++++++++++ -``indico_sixpay`` - SIX EPayment Plugin for Indico -++++++++++++++++++++++++++++++++++++++++++++++++++ - -.. image:: https://readthedocs.org/projects/indico_sixpay/badge/?version=latest - :target: http://indico-sixpay.readthedocs.io/en/latest/?badge=latest - :alt: Documentation - -.. image:: https://img.shields.io/pypi/v/indico_sixpay.svg - :alt: Available on PyPI - :target: https://pypi.python.org/pypi/indico_sixpay/ - -.. image:: https://img.shields.io/github/license/maxfischer2781/indico_sixpay.svg - :alt: License - :target: https://github.com/maxfischer2781/indico_sixpay/blob/master/LICENSE - -.. image:: https://img.shields.io/github/commits-since/maxfischer2781/indico_sixpay/v2.0.0.svg - :alt: Repository - :target: https://github.com/maxfischer2781/indico_sixpay/tree/master - -Plugin for the Indico event management system to use EPayment via SIX Payment services. - -Quick Guide ------------ - -To enable the plugin, it must be installed for the python version running ``indico``. - -.. code:: bash - - python -m pip install indico_sixpay - -Once installed, it can be enabled in the administrator and event settings. -Configuration uses the same options for global defaults and event specific overrides. - -Disclaimer ----------- - -This plugin is in no way endorsed, supported or provided by SIX, Indico or any other service, provider or entity. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" -__title__ = 'indico-plugin-sixpay' -__summary__ = 'Indico EPayment Plugin for SixPay services' -__url__ = 'https://github.com/maxfischer2781/indico_sixpay' - -__version__ = '3.0-dev' -__author__ = 'Max Fischer' -__email__ = 'maxfischer2781@gmail.com' -__copyright__ = '2017 - 2018 %s' % __author__ diff --git a/payment_sixpay/indico_sixpay/blueprint.py b/payment_sixpay/indico_sixpay/blueprint.py deleted file mode 100644 index bc724a3..0000000 --- a/payment_sixpay/indico_sixpay/blueprint.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file is part of the Indico plugins. -# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN -# -# The Indico plugins are free software; you can redistribute -# them and/or modify them under the terms of the MIT License; -# see the LICENSE file for more details. -"""Definition of callbacks exposed by the Indico server.""" - -from indico.core.plugins import IndicoPluginBlueprint - -from .request_handlers import SixPayResponseHandler, UserCancelHandler, UserFailureHandler, UserSuccessHandler - - -#: url mount points exposing callbacks -blueprint = IndicoPluginBlueprint( - 'payment_sixpay', __name__, - url_prefix='/event//registrations//payment/response/sixpay' -) - -blueprint.add_url_rule( - '/failure', 'failure', UserCancelHandler, methods=('GET', 'POST') -) -blueprint.add_url_rule( - '/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST') -) -blueprint.add_url_rule( - '/success', 'success', UserSuccessHandler, methods=('GET', 'POST') -) -# Used by SixPay to send an asynchronous notification for the transaction -blueprint.add_url_rule( - '/ipn', 'notify', SixPayResponseHandler, methods=('Get', 'POST') -) diff --git a/payment_sixpay/indico_sixpay/plugin.py b/payment_sixpay/indico_sixpay/plugin.py deleted file mode 100644 index 2aec664..0000000 --- a/payment_sixpay/indico_sixpay/plugin.py +++ /dev/null @@ -1,391 +0,0 @@ -# This file is part of the Indico plugins. -# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN -# -# The Indico plugins are free software; you can redistribute -# them and/or modify them under the terms of the MIT License; -# see the LICENSE file for more details. -""" -Core of the SixPay plugin. - -The entry point for indico is the :py:class:`~.SixpayPaymentPlugin`. -It handles configuration via the settings forms, initiates payments -and provides callbacks for finished payments via its blueprint. -""" - -import requests -from urllib.parse import urljoin -from werkzeug.exceptions import InternalServerError as HTTPInternalServerError -from werkzeug.exceptions import NotImplemented as HTTPNotImplemented -from wtforms.fields import StringField -from wtforms.fields.html5 import URLField -from wtforms.validators import DataRequired, Email, Length, Optional, Regexp, ValidationError - -from indico.core.plugins import IndicoPlugin, url_for_plugin -from indico.modules.events.payment import (PaymentEventSettingsFormBase, PaymentPluginMixin, - PaymentPluginSettingsFormBase) -from indico.modules.events.payment.models.transactions import PaymentTransaction, TransactionAction -from indico.web.forms.fields import IndicoPasswordField - -# blueprint mounts the request handlers onto URLs -from .blueprint import blueprint -from .utility import (get_request_header, get_terminal_id, gettext, provider, saferpay_json_api_spec, - saferpay_pp_init_url, to_small_currency) - - -# Dear Future Maintainer, -# -# while an improvement over Indico 1.2, the Indico 2.0 plugin/core -# facilities are rather lacking in documentation. I have added -# some notes for each **base**type on how they are integrated with -# the rest of Indico. Be aware that this is reconstructed from -# implementations, not from official API docs. -# -# Regards, -# Past Maintainer - - -# PaymentPluginSettingsFormBase from indico.modules.events.payment -# - A codified Form for users to fill in. The *class attributes* define -# which fields exist, their shape, description, etc. -# - Each field is a type from wtforms.fields.core.Field. You probably want: -# - label: Name of the field, an internationalised identifier -# - validators: Input validation, see wtforms.validators -# - description: help text of the field, an internationalised text - -class FormatField: - """Validator for format fields, i.e. strings with ``{key}`` placeholders. - - :param max_length: optional maximum length, checked on a test formatting - :type max_length: int - :param field_map: keyword arguments to use for test formatting - - On validation, a test mapping is applied to the field. This ensures the - field has a valid ``str.format`` format, and does not use illegal keys - (as determined by ``default_field_map`` and ``field_map``). - The ``max_length`` is validated against the test-formatted field, which - is an estimate for an average sized input. - """ - - #: default placeholders to test length after formatting - default_field_map = { - 'user_id': 1234, - 'user_name': 'Jane Whiteacre', - 'event_id': 123, - 'event_title': 'Placeholder: The Event', - 'eventuser_id': 'e123u1234', - 'registration_title': 'EarlyBird Registration' - } - - def __init__(self, max_length=float('inf'), field_map=None): - """Format field validator, i.e. strings with ``{key}`` placeholders. - - :param max_length: optional maximum length, - checked on a test formatting - :type max_length: int - :param field_map: keyword arguments to use for test formatting - """ - self.max_length = max_length - self.field_map = self.default_field_map.copy() - if field_map is not None: - self.field_map.update(field_map) - - def __call__(self, form, field): - """Validate format field data. - - Returns true on successful validation, else an ValidationError is - thrown. - """ - if not field.data: - return True - try: - test_format = field.data.format(**self.field_map) - except KeyError as err: - raise ValidationError(f'Invalid format string key: {err}') - except ValueError as err: - raise ValidationError(f'Malformed format string: {err}') - if len(test_format) > self.max_length: - raise ValidationError( - 'Too long format string:' - ' shortest replacement with {}, expected {}' - .format( - len(test_format), self.max_length - ) - ) - else: - return True - - -class PluginSettingsForm(PaymentPluginSettingsFormBase): - """Configuration form for the Plugin across all events.""" - - url = URLField( - label=gettext('SixPay Saferpay URL'), - validators=[DataRequired()], - description=gettext('URL to contact the Six Payment Service'), - ) - username = StringField( - label=gettext('Username'), - validators=[DataRequired()], - description=gettext('SaferPay JSON API User name.') - ) - password = IndicoPasswordField( - label=gettext('Password'), - validators=[DataRequired()], - description=gettext('SaferPay JSON API User password.'), - toggle=True, - ) - account_id = StringField( - label='Account ID', - # can be set EITHER or BOTH globally and per event - validators=[ - Optional(), - Regexp( - r'[0-9-]{0,15}', - message='Field must contain up to 15 digits or "-".' - ) - ], - description=gettext( - 'Default ID of your Saferpay account,' - ' such as "401860-17795278".' - ) - ) - order_description = StringField( - label=gettext('Order Description'), - validators=[DataRequired(), FormatField(max_length=80)], - description=gettext( - 'The description of each order in a human readable way. ' - 'This description is presented to the registrant during the ' - 'transaction with SixPay.' - ) - ) - order_identifier = StringField( - label=gettext('Order Identifier'), - validators=[DataRequired(), FormatField(max_length=80)], - description=gettext( - 'The identifier of each order for further processing.' - ) - ) - notification_mail = StringField( - label=gettext('Notification Email'), - validators=[Optional(), Email(), Length(0, 50)], - description=gettext( - 'Mail address to receive notifications of transactions.' - "This is independent of Indico's own payment notifications." - ) - ) - - -class EventSettingsForm(PaymentEventSettingsFormBase): - """Configuration form for the Plugin for a specific event.""" - - # every setting may be overwritten for each event - account_id = PluginSettingsForm.account_id - order_description = PluginSettingsForm.order_description - order_identifier = PluginSettingsForm.order_identifier - notification_mail = PluginSettingsForm.notification_mail - - -# PaymentPluginMixin, IndicoPlugin -# This is basically a registry of setting fields, -# logos and other rendering stuff. -# All the business logic is in :py:func:`adjust_payment_form_data` -class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): - """SixPay Saferpay plugin. - - Provides an EPayment method using the SixPay Saferpay API. - """ - - configurable = True - #: form for default configuration across events - settings_form = PluginSettingsForm - #: form for configuration for specific events - event_settings_form = EventSettingsForm - #: global default settings - should be a reasonable default - default_settings = { - 'method_name': 'SixPay', - 'url': 'https://www.saferpay.com/api/', - 'username': None, - 'password': None, - 'account_id': None, - 'order_description': - '{event_title}, {registration_title}, {user_name}', - 'order_identifier': '{eventuser_id}', - 'notification_mail': None - } - #: per event default settings - use the global settings - default_event_settings = { - 'enabled': False, - 'method_name': None, - # 'url': None, - # 'username': None, - # 'password': None, - 'account_id': None, - 'order_description': None, - 'order_identifier': None, - 'notification_mail': None, - } - - def get_blueprints(self): - """Blueprint for URL endpoints with callbacks.""" - return blueprint - - # Dear Future Maintainer, - # - business logic is here! - # - see PaymentPluginMixin.render_payment_form for what `data` provides - # - What happens here - # - We add `success`, `cancel` and `failure` for *sixpay* to redirect the - # user back to us AFTER his request - # - We add `notify` for *sixpay* to inform us asynchronously about - # the result - # - We send a request to initialize the pyment page to SixPay to get a - # request url for this transaction - # - We put the payment page URL and token we got into `data` - # - Return uses `indico_sixpay/templates/event_payment_form.html`, - # presenting a trigger button to the user - def adjust_payment_form_data(self, data): - """Prepare the payment form shown to registrants.""" - # indico does not seem to provide stacking of settings - # we merge event on top of global settings, but remove defaults - event_settings = data['event_settings'] - global_settings = data['settings'] - plugin_settings = { - key: event_settings[key] - if event_settings.get(key) is not None - else global_settings[key] - for key in (set(event_settings) | set(global_settings)) - } - # parameters of the transaction - amount, currency, ... - transaction = self._get_transaction_parameters(data, plugin_settings) - init_response = self._init_payment_page( - sixpay_url=plugin_settings['url'], - transaction_data=transaction, - credentials=( - plugin_settings['username'], plugin_settings['password']) - ) - data['payment_url'] = init_response['RedirectUrl'] - - # create pending transaction and store Saferpay transaction token - if not PaymentTransaction.create_next( - registration=data['registration'], - amount=data['amount'], - currency=data['currency'], - action=TransactionAction.pending, - provider=provider, - data={'Init_PP_response': init_response} - ): - data['registration'].transaction.data = { - 'Init_PP_response': init_response - } - return data - - @staticmethod - def get_field_format_map(registration): - """Generate dict which provides registration information.""" - return { - 'user_id': registration.user_id, - 'user_name': registration.full_name, - 'user_firstname': registration.first_name, - 'user_lastname': registration.last_name, - 'event_id': registration.event_id, - 'event_title': registration.event.title, - 'eventuser_id': - f'e{registration.event_id}u{registration.user_id}', - 'registration_title': registration.registration_form.title - } - - def _get_transaction_parameters(self, payment_data, plugin_settings): - """Parameters for formulating a transaction request.""" - registration = payment_data['registration'] - format_map = self.get_field_format_map(registration) - for format_field in 'order_description', 'order_identifier': - try: - payment_data[format_field] = ( - plugin_settings[format_field].format(**format_map) - ) - except ValueError: - message = 'Invalid format field placeholder for {0}, please contact the event organisers!' - raise HTTPNotImplemented((gettext(message) + '\n\n[' + message + ']').format(self.name)) - except KeyError: - message = 'Unknown format field placeholder "{0}" for {1}, please contact the event organisers!' - raise HTTPNotImplemented((gettext(message) + '\n\n[' + message + ']').format(format_field, self.name)) - - # see the SixPay Manual - # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize - # on what these things mean - transaction_parameters = { - 'RequestHeader': get_request_header( - saferpay_json_api_spec, plugin_settings['account_id'] - ), - 'TerminalId': str( - get_terminal_id(plugin_settings['account_id']) - ), - 'Payment': { - 'Amount': { - # indico handles price as largest currency, but six expects - # smallest. E.g. EUR: indico uses 100.2 Euro, but six - # expects 10020 Cent - 'Value': '{:d}'.format( - to_small_currency( - payment_data['amount'], - payment_data['currency'] - ) - ), - 'CurrencyCode': payment_data['currency'], - }, - 'OrderId': payment_data['order_identifier'][:80], - 'DESCRIPTION': payment_data['order_description'][:1000], - }, - # callbacks of the transaction - where to announce success, ... - # where to redirect the user - 'ReturnUrls': { - 'Success': url_for_plugin( - 'payment_sixpay.success', - registration.locator.uuid, - _external=True - ), - 'Fail': url_for_plugin( - 'payment_sixpay.failure', - registration.locator.uuid, - _external=True - ), - 'Abort': url_for_plugin( - 'payment_sixpay.cancel', - registration.locator.uuid, - _external=True - ) - }, - 'Notification': { - # where to asynchronously call back from SixPay - 'NotifyUrl': url_for_plugin( - 'payment_sixpay.notify', - registration.locator.uuid, - _external=True - ) - } - } - if 'notification_mail' in plugin_settings: - transaction_parameters['Notification']['MerchantEmails'] = ( - plugin_settings['notification_mail'] - ) - return transaction_parameters - - def _init_payment_page(self, sixpay_url, transaction_data, credentials): - """Initialize payment page.""" - endpoint = urljoin(sixpay_url, saferpay_pp_init_url) - url_request = requests.post( - endpoint, - json=transaction_data, - auth=credentials - ) - # raise any HTTP errors - url_request.raise_for_status() - response = url_request.json() - if 'ErrorName' in response: - if 'ErrorDetail' not in response: - response['ErrorDetail'] = '' - raise HTTPInternalServerError( - 'Failed request to SixPay service:' - ' {ErrorMessage}. {ErrorDetail}' - .format(**response) - ) - return response diff --git a/payment_sixpay/indico_sixpay/request_handlers.py b/payment_sixpay/indico_sixpay/request_handlers.py deleted file mode 100644 index 2682c34..0000000 --- a/payment_sixpay/indico_sixpay/request_handlers.py +++ /dev/null @@ -1,329 +0,0 @@ -# This file is part of the Indico plugins. -# Copyright (C) 2017 - 2021 Max Fischer, Martin Claus, CERN -# -# The Indico plugins are free software; you can redistribute -# them and/or modify them under the terms of the MIT License; -# see the LICENSE file for more details. -"""Callbacks for asynchronous replies by Saferpay and to redirect the user.""" - -import requests -from urllib.parse import urljoin -from flask import flash, redirect, request -from flask_pluginengine import current_plugin -from werkzeug.exceptions import BadRequest - -from indico.modules.events.payment.models.transactions import TransactionAction -from indico.modules.events.payment.notifications import notify_amount_inconsistency -from indico.modules.events.registration.models.registrations import Registration -from indico.web.flask.util import url_for -from indico.web.rh import RH - -from .utility import (get_request_header, get_setting, gettext, provider, saferpay_json_api_spec, - saferpay_pp_assert_url, saferpay_pp_cancel_url, saferpay_pp_capture_url, to_large_currency, - to_small_currency) - - -# RH from indico.web.rh -# - the logic to execute when SixPay/Users are redirected *after* a transaction -# - see blueprint.py for how RH's are mounted! - - -class BaseRequestHandler(RH): - """Request Handler for asynchronous callbacks from SixPay. - - These handlers are used either by - - - the user, when he is redirected from SixPay back to Indico - - SixPay, when it sends back the result of a transaction - """ - - CSRF_ENABLED = False - - def _process_args(self): - self.registration = Registration.query.filter_by(uuid=request.args['token']).first() - if not self.registration: - raise BadRequest - self.token = ( - self.registration.transaction.data['Init_PP_response']['Token'] - ) - - def _get_setting(self, setting): - return get_setting(setting, self.registration.registration_form.event) - - -class TransactionFailure(Exception): - """A Transaction with SixPay failed. - - :param step: name of the step at which the transaction failed - :type step: basestring - :param details: verbose description of what went wrong - :type step: basestring - """ - - def __init__(self, step, details=None): - """Initialize request handler.""" - self.step = step - self.details = details - - -class SixPayResponseHandler(BaseRequestHandler): - """Handler for notification from SixPay service.""" - - def __init__(self): - """Initialize request handler.""" - super().__init__() - # registration context is not initialised before `self._process_args` - self.sixpay_url = None # type: str - - def _process_args(self): - super()._process_args() - self.sixpay_url = get_setting('url') - - def _process(self): - """Process the reply from SixPay about the transaction.""" - try: - self._process_confirmation() - except TransactionFailure as err: - current_plugin.logger.warning( - 'SixPay transaction failed during %s: %s' - % (err.step, err.details) - ) - - def _process_confirmation(self): - """Process the confirmation response inside indico.""" - transaction_data = request.json - # transaction_signature = request.args['SIGNATURE'] - # transaction_data = self._parse_transaction_xml(transaction_xml) - # assert transaction status from SixPay - try: - assert_response = self._assert_payment(transaction_data) - if self._is_duplicate_transaction(assert_response): - # we have already handled the transaction - return True - if ( - self._is_authorized(assert_response) - and not self._is_captured(assert_response) - ): - capture_response = self._capture_transaction(assert_response) - assert_response['CaptureResponse'] = capture_response - self._verify_amount(assert_response) - self._register_payment(assert_response) - except TransactionFailure as err: - current_plugin.logger.warning( - 'SixPay transaction failed during %s: %s' - % (err.step, err.details) - ) - raise - return True - - def _perform_request(self, task, endpoint, data): - """Perform a request against SixPay. - - :param task: description of the request, used for error handling - :type task: basestring - :param endpoint: the URL endpoint *relative* to the SixPay base URL - :type endpoint: basestring - :param **data: data passed during the request - - This will automatically raise any HTTP errors encountered during the - request. If the request itself fails, a :py:exc:`~.TransactionFailure` - is raised for ``task``. - """ - request_url = urljoin(self.sixpay_url, endpoint) - credentials = ( - get_setting('username'), - get_setting('password') - ) - response = requests.post( - request_url, json=data, auth=credentials - ) - try: - response.raise_for_status() - except requests.HTTPError: - raise TransactionFailure( - step=task, - details=response.text - ) - return response - - def _assert_payment(self, transaction_data): - """Check the status of the transaction with SixPay. - - Returns transaction assert data. - """ - assert_response = self._perform_request( - 'assert', - saferpay_pp_assert_url, - { - 'RequestHeader': get_request_header( - saferpay_json_api_spec, self._get_setting('account_id') - ), - 'Token': self.token, - } - ) - if assert_response.status_code == requests.codes.ok: - return assert_response.json() - - def _is_duplicate_transaction(self, transaction_data): - """Check if this transaction has already been recorded.""" - prev_transaction = self.registration.transaction - if ( - not prev_transaction - or prev_transaction.provider != 'sixpay' - or 'Transaction' not in prev_transaction.data - ): - return False - old = prev_transaction.data.get('Transaction') - new = transaction_data.get('Transaction') - return ( - old['OrderId'] == new['OrderId'] - & old['Type'] == new['Type'] - & old['Id'] == new['Id'] - & old['SixTransactionReference'] == new['SixTransactionReference'] - & old['Amount']['Value'] == new['Amount']['Value'] - & old['Amount']['CurrencyCode'] == new['Amount']['CurrencyCode'] - ) - - def _is_authorized(self, assert_data): - """Check if payment is authorized.""" - return assert_data['Transaction']['Status'] == 'AUTHORIZED' - - def _is_captured(self, assert_data): - """Check if payment is captured, i.e. the cash flow is triggered.""" - return assert_data['Transaction']['Status'] == 'CAPTURED' - - def _verify_amount(self, assert_data): - """Verify the amount and currency of the payment. - - Sends an email but still registers incorrect payments. - """ - expected_amount = float(self.registration.price) - expected_currency = self.registration.currency - amount = float(assert_data['Transaction']['Amount']['Value']) - currency = assert_data['Transaction']['Amount']['CurrencyCode'] - if ( - to_small_currency(expected_amount, expected_currency) == amount - and expected_currency == currency - ): - return True - current_plugin.logger.warning( - "Payment doesn't match events fee: %s %s != %s %s", - amount, currency, - to_small_currency(expected_amount, expected_currency), - expected_currency - ) - notify_amount_inconsistency( - self.registration, - to_large_currency(amount, currency), - currency - ) - return False - - def _capture_transaction(self, assert_data): - """Confirm to SixPay that the transaction is accepted. - - On success returns the response JSON data. - """ - capture_data = { - 'RequestHeader': get_request_header( - saferpay_json_api_spec, self._get_setting('account_id') - ), - 'TransactionReference': { - 'TransactionId': assert_data['Transaction']['Id'] - } - } - capture_response = self._perform_request( - 'capture', saferpay_pp_capture_url, capture_data - ) - return capture_response.json() - - def _cancel_transaction(self, assert_data): - """Inform Sixpay that the transaction is canceled. - - Cancel the transaction at Sixpay. This method is implemented but - not used and tested yet. - """ - cancel_data = { - 'RequestHeader': get_request_header( - saferpay_json_api_spec, self._get_setting('account_id') - ), - 'TransactionReference': { - 'TransactionId': assert_data['Transaction']['Id'] - } - } - cancel_response = self._perform_request( - 'cancel', saferpay_pp_cancel_url, cancel_data - ) - return cancel_response.json() - - def _register_payment(self, assert_data): - """Register the transaction as paid.""" - self.registration.transaction.create_next( - registration=self.registration, - amount=self.registration.transaction.amount, - currency=self.registration.transaction.currency, - action=TransactionAction.complete, - provider=provider - ) - self.registration.update_state(paid=True) - - -class UserCancelHandler(BaseRequestHandler): - """User Message on cancelled payment.""" - - def _process(self): - self.registration.transaction.create_next( - registration=self.registration, - amount=self.registration.transaction.amount, - currency=self.registration.transaction.currency, - action=TransactionAction.reject, - provider=provider - ) - flash(gettext('You cancelled the payment.'), 'info') - return redirect(url_for( - 'event_registration.display_regform', - self.registration.locator.registrant) - ) - - -class UserFailureHandler(BaseRequestHandler): - """User Message on failed payment.""" - - def _process(self): - self.registration.transaction.create_next( - registration=self.registration, - amount=self.registration.transaction.amount, - currency=self.registration.transaction.currency, - action=TransactionAction.reject, - provider=provider - ) - flash(gettext('Your payment has failed.'), 'info') - return redirect(url_for( - 'event_registration.display_regform', - self.registration.locator.registrant) - ) - - -class UserSuccessHandler(SixPayResponseHandler): - """User Message on successful payment.""" - - def _process(self): - try: - self._process_confirmation() - except TransactionFailure as err: - current_plugin.logger.warning( - 'SixPay transaction failed during {}: {}'.format( - err.step, err.details - ) - ) - flash(gettext( - 'Your payment could not be confirmed.' - ' Please contact an organizer.'), - 'info' - ) - else: - flash(gettext('Your payment has been confirmed.'), 'success') - return redirect(url_for( - 'event_registration.display_regform', - self.registration.locator.registrant) - ) diff --git a/payment_sixpay/setup.cfg b/payment_sixpay/setup.cfg new file mode 100644 index 0000000..6ee7da7 --- /dev/null +++ b/payment_sixpay/setup.cfg @@ -0,0 +1,33 @@ +[metadata] +name = indico-plugin-payment-sixpay +version = 3.0 +description = SIXPay payments for Indico event registration fees +long_description = file: README.md +long_description_content_type = text/markdown; charset=UTF-8; variant=GFM +url = https://github.com/indico/indico-plugins +license = MIT +author = Max Fischer, Martin Claus and Indico Team (CERN) +author_email = indico-team@cern.ch +classifiers = + Environment :: Plugins + Environment :: Web Environment + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3.9 + +[options] +packages = find: +zip_safe = false +include_package_data = true +python_requires = ~=3.9.0 +install_requires = + indico>=3.0 + iso4217==1.6.20180829 + +[options.entry_points] +indico.plugins = + payment_sixpay = indico_payment_sixpay.plugin:SixpayPaymentPlugin + + + +[pydocstyle] +ignore = D100,D101,D102,D103,D104,D105,D107,D203,D213 diff --git a/payment_sixpay/setup.py b/payment_sixpay/setup.py index 071a172..1cb2c6f 100644 --- a/payment_sixpay/setup.py +++ b/payment_sixpay/setup.py @@ -4,48 +4,8 @@ # The Indico plugins are free software; you can redistribute # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. -import os -from setuptools import find_packages, setup +from setuptools import setup -repo_base_dir = os.path.abspath(os.path.dirname(__file__)) -# pull in the packages metadata -package_about = {} -with open(os.path.join(repo_base_dir, 'indico_sixpay', '__about__.py')) as about_file: - exec(about_file.read(), package_about) - -setup( - name=package_about['__title__'], - version=package_about['__version__'], - description=package_about['__summary__'], - long_description=package_about['__doc__'].strip(), - author=package_about['__author__'], - author_email=package_about['__email__'], - url=package_about['__url__'], - entry_points={ - 'indico.plugins': { - 'payment_sixpay = indico_sixpay.plugin:SixpayPaymentPlugin' - } - }, - packages=find_packages(), - package_data={'indico_sixpay': ['templates/*.html']}, - install_requires=['requests', 'indico>=3.0', 'iso4217'], - python_requires='~=3.9.0', - license='MIT', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Plugins', - 'Environment :: Web Environment', - 'Intended Audience :: Science/Research', - 'Intended Audience :: System Administrators', - 'Intended Audience :: Other Audience', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Topic :: Communications :: Conferencing', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - ], - zip_safe=False, - keywords='indico epayment six sixpay plugin', -) +setup() From 613e2cc50ae75c8c7629d39c508f2e18b8448166 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Thu, 29 Jul 2021 11:31:29 +0200 Subject: [PATCH 13/14] Only create pending txn when proceeding to payment Otherwise someone may go to the checkout page not proceed, and then no longer see the checkout link due to the pending transaction. While this can still happen now if the user simply interrupts the payment process instead of finishing or cancelling, it's much less likely to happen (and an event manager can always "mark unpaid" to reset the pending transaction in case a user ends up in this state) --- .../indico_payment_sixpay/blueprint.py | 9 +- .../indico_payment_sixpay/controllers.py | 101 ++++++++++++++- .../indico_payment_sixpay/plugin.py | 116 +----------------- .../templates/event_payment_form.html | 11 +- .../templates/transaction_details.html | 2 + 5 files changed, 114 insertions(+), 125 deletions(-) diff --git a/payment_sixpay/indico_payment_sixpay/blueprint.py b/payment_sixpay/indico_payment_sixpay/blueprint.py index e12a9a0..0019b59 100644 --- a/payment_sixpay/indico_payment_sixpay/blueprint.py +++ b/payment_sixpay/indico_payment_sixpay/blueprint.py @@ -7,16 +7,17 @@ from indico.core.plugins import IndicoPluginBlueprint -from indico_payment_sixpay.controllers import (SixPayNotificationHandler, UserCancelHandler, UserFailureHandler, - UserSuccessHandler) +from indico_payment_sixpay.controllers import (RHInitSixpayPayment, SixPayNotificationHandler, UserCancelHandler, + UserFailureHandler, UserSuccessHandler) blueprint = IndicoPluginBlueprint( 'payment_sixpay', __name__, - url_prefix='/event//registrations//payment/response/sixpay' + url_prefix='/event//registrations//payment/sixpay' ) +blueprint.add_url_rule('/init', 'init', RHInitSixpayPayment, methods=('GET', 'POST')) blueprint.add_url_rule('/failure', 'failure', UserCancelHandler, methods=('GET', 'POST')) blueprint.add_url_rule('/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST')) blueprint.add_url_rule('/success', 'success', UserSuccessHandler, methods=('GET', 'POST')) -blueprint.add_url_rule('/ipn', 'notify', SixPayNotificationHandler, methods=('Get', 'POST')) +blueprint.add_url_rule('/notify', 'notify', SixPayNotificationHandler, methods=('Get', 'POST')) diff --git a/payment_sixpay/indico_payment_sixpay/controllers.py b/payment_sixpay/indico_payment_sixpay/controllers.py index 168641e..2beba3f 100644 --- a/payment_sixpay/indico_payment_sixpay/controllers.py +++ b/payment_sixpay/indico_payment_sixpay/controllers.py @@ -9,11 +9,14 @@ from urllib.parse import urljoin import requests from flask import flash, redirect, request -from werkzeug.exceptions import BadRequest +from requests import RequestException +from werkzeug.exceptions import BadRequest, NotFound +from indico.core.plugins import url_for_plugin +from indico.modules.events.payment.controllers import RHPaymentBase from indico.modules.events.payment.models.transactions import TransactionAction from indico.modules.events.payment.notifications import notify_amount_inconsistency -from indico.modules.events.payment.util import register_transaction +from indico.modules.events.payment.util import get_active_payment_plugins, register_transaction from indico.modules.events.registration.models.registrations import Registration from indico.web.flask.util import url_for from indico.web.rh import RH @@ -21,8 +24,9 @@ from indico.web.rh import RH from indico_payment_sixpay import _ from indico_payment_sixpay.plugin import SixpayPaymentPlugin from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, SIXPAY_PP_ASSERT_URL, - SIXPAY_PP_CANCEL_URL, SIXPAY_PP_CAPTURE_URL, get_request_header, get_setting, - to_large_currency, to_small_currency) + SIXPAY_PP_CANCEL_URL, SIXPAY_PP_CAPTURE_URL, SIXPAY_PP_INIT_URL, + get_request_header, get_setting, get_terminal_id, to_large_currency, + to_small_currency) class TransactionFailure(Exception): @@ -58,6 +62,95 @@ class RHSixpayBase(RH): return get_setting(setting, self.registration.event) +class RHInitSixpayPayment(RHPaymentBase): + def _get_transaction_parameters(self): + """Get parameters for creating a transaction request.""" + settings = SixpayPaymentPlugin.event_settings.get_all(self.event) + format_map = { + 'user_id': self.registration.user_id, + 'user_name': self.registration.full_name, + 'user_firstname': self.registration.first_name, + 'user_lastname': self.registration.last_name, + 'event_id': self.registration.event_id, + 'event_title': self.registration.event.title, + 'registration_id': self.registration.id, + 'regform_title': self.registration.registration_form.title + } + order_description = settings['order_description'].format(**format_map) + order_identifier = settings['order_identifier'].format(**format_map) + # see the SixPay Manual + # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize + # on what these things mean + transaction_parameters = { + 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, settings['account_id']), + 'TerminalId': get_terminal_id(settings['account_id']), + 'Payment': { + 'Amount': { + # indico handles price as largest currency, but six expects + # smallest. E.g. EUR: indico uses 100.2 Euro, but six + # expects 10020 Cent + 'Value': str(to_small_currency(self.registration.price, self.registration.currency)), + 'CurrencyCode': self.registration.currency, + }, + 'OrderId': order_identifier[:80], + 'DESCRIPTION': order_description[:1000], + }, + # callbacks of the transaction - where to announce success etc., when redircting the user + 'ReturnUrls': { + 'Success': url_for_plugin('payment_sixpay.success', self.registration.locator.uuid, _external=True), + 'Fail': url_for_plugin('payment_sixpay.failure', self.registration.locator.uuid, _external=True), + 'Abort': url_for_plugin('payment_sixpay.cancel', self.registration.locator.uuid, _external=True) + }, + 'Notification': { + # where to asynchronously call back from SixPay + 'NotifyUrl': url_for_plugin('payment_sixpay.notify', self.registration.locator.uuid, _external=True) + } + } + if settings['notification_mail']: + transaction_parameters['Notification']['MerchantEmails'] = [settings['notification_mail']] + return transaction_parameters + + def _init_payment_page(self, transaction_data): + """Initialize payment page.""" + endpoint = urljoin(SixpayPaymentPlugin.settings.get('url'), SIXPAY_PP_INIT_URL) + credentials = (SixpayPaymentPlugin.settings.get('username'), SixpayPaymentPlugin.settings.get('password')) + resp = requests.post(endpoint, json=transaction_data, auth=credentials) + try: + resp.raise_for_status() + except RequestException as exc: + self.logger.error('Could not initialize payment: %s', exc.response.text) + raise Exception('Could not initialize payment') + return resp.json() + + def _process_args(self): + RHPaymentBase._process_args(self) + if 'sixpay' not in get_active_payment_plugins(self.event): + raise NotFound + if not SixpayPaymentPlugin.instance.supports_currency(self.registration.currency): + raise BadRequest + + def _process(self): + transaction_params = self._get_transaction_parameters() + init_response = self._init_payment_page(transaction_params) + payment_url = init_response['RedirectUrl'] + + # create pending transaction and store Saferpay transaction token + new_indico_txn = register_transaction( + self.registration, + self.registration.price, + self.registration.currency, + TransactionAction.pending, + PROVIDER_SIXPAY, + {'Init_PP_response': init_response} + ) + if not new_indico_txn: + # set it on the current transaction if we could not create a next one + # this happens if we already have a pending transaction and it's incredibly + # ugly... + self.registration.transaction.data = {'Init_PP_response': init_response} + return redirect(payment_url) + + class SixPayNotificationHandler(RHSixpayBase): """Handler for notification from SixPay service.""" diff --git a/payment_sixpay/indico_payment_sixpay/plugin.py b/payment_sixpay/indico_payment_sixpay/plugin.py index 959ba4b..3f2ec96 100644 --- a/payment_sixpay/indico_payment_sixpay/plugin.py +++ b/payment_sixpay/indico_payment_sixpay/plugin.py @@ -5,19 +5,10 @@ # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. -from urllib.parse import urljoin - -import requests -from requests import RequestException - -from indico.core.plugins import IndicoPlugin, url_for_plugin +from indico.core.plugins import IndicoPlugin from indico.modules.events.payment import PaymentPluginMixin -from indico.modules.events.payment.models.transactions import TransactionAction -from indico.modules.events.payment.util import register_transaction from indico_payment_sixpay.forms import EventSettingsForm, PluginSettingsForm -from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, SIXPAY_PP_INIT_URL, get_request_header, - get_terminal_id, to_small_currency) class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): @@ -56,108 +47,3 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): """Blueprint for URL endpoints with callbacks.""" from indico_payment_sixpay.blueprint import blueprint return blueprint - - # Dear future Maintainer, - # - the business logic is here! - # - see PaymentPluginMixin.render_payment_form for what `data` provides - # - What happens here - # - We add `success`, `cancel` and `failure` for *sixpay* to redirect the - # user back to us AFTER his request - # - We add `notify` for *sixpay* to inform us asynchronously about - # the result - # - We send a request to initialize the pyment page to SixPay to get a - # request url for this transaction - # - We put the payment page URL and token we got into `data` - # - Return uses `indico_payment_sixpay/templates/event_payment_form.html`, - # presenting a trigger button to the user - def adjust_payment_form_data(self, data): - """Prepare the payment form shown to registrants.""" - global_settings = data['settings'] - transaction = self._get_transaction_parameters(data) - init_response = self._init_payment_page( - sixpay_url=global_settings['url'], - transaction_data=transaction, - credentials=(global_settings['username'], global_settings['password']) - ) - data['payment_url'] = init_response['RedirectUrl'] - - # create pending transaction and store Saferpay transaction token - new_indico_txn = register_transaction( - data['registration'], - data['amount'], - data['currency'], - TransactionAction.pending, - PROVIDER_SIXPAY, - {'Init_PP_response': init_response} - ) - if not new_indico_txn: - # set it on the current transaction if we could not create a next one - # this happens if we already have a pending transaction and it's incredibly - # ugly... - data['registration'].transaction.data = {'Init_PP_response': init_response} - return data - - @staticmethod - def get_field_format_map(registration): - """Generate dict which provides registration information.""" - return { - 'user_id': registration.user_id, - 'user_name': registration.full_name, - 'user_firstname': registration.first_name, - 'user_lastname': registration.last_name, - 'event_id': registration.event_id, - 'event_title': registration.event.title, - 'registration_id': registration.id, - 'regform_title': registration.registration_form.title - } - - def _get_transaction_parameters(self, payment_data): - """Parameters for formulating a transaction request.""" - settings = payment_data['event_settings'] - registration = payment_data['registration'] - format_map = self.get_field_format_map(registration) - for format_field in ('order_description', 'order_identifier'): - payment_data[format_field] = settings[format_field].format(**format_map) - - # see the SixPay Manual - # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize - # on what these things mean - transaction_parameters = { - 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, settings['account_id']), - 'TerminalId': get_terminal_id(settings['account_id']), - 'Payment': { - 'Amount': { - # indico handles price as largest currency, but six expects - # smallest. E.g. EUR: indico uses 100.2 Euro, but six - # expects 10020 Cent - 'Value': str(to_small_currency(payment_data['amount'], payment_data['currency'])), - 'CurrencyCode': payment_data['currency'], - }, - 'OrderId': payment_data['order_identifier'][:80], - 'DESCRIPTION': payment_data['order_description'][:1000], - }, - # callbacks of the transaction - where to announce success etc., when redircting the user - 'ReturnUrls': { - 'Success': url_for_plugin('payment_sixpay.success', registration.locator.uuid, _external=True), - 'Fail': url_for_plugin('payment_sixpay.failure', registration.locator.uuid, _external=True), - 'Abort': url_for_plugin('payment_sixpay.cancel', registration.locator.uuid, _external=True) - }, - 'Notification': { - # where to asynchronously call back from SixPay - 'NotifyUrl': url_for_plugin('payment_sixpay.notify', registration.locator.uuid, _external=True) - } - } - if settings['notification_mail']: - transaction_parameters['Notification']['MerchantEmails'] = [settings['notification_mail']] - return transaction_parameters - - def _init_payment_page(self, sixpay_url, transaction_data, credentials): - """Initialize payment page.""" - endpoint = urljoin(sixpay_url, SIXPAY_PP_INIT_URL) - resp = requests.post(endpoint, json=transaction_data, auth=credentials) - try: - resp.raise_for_status() - except RequestException as exc: - self.logger.error('Could not initialize payment: %s', exc.response.text) - raise Exception('Could not initialize payment') - return resp.json() diff --git a/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html b/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html index e6d6afb..8d04287 100644 --- a/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html +++ b/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html @@ -1,4 +1,7 @@ -Clicking on the {% trans %}Pay Now{% endtrans %} button you will get redirected to the SixPay site in order to complete your transaction. +{% trans %} + Clicking on the Pay Now button will redirect you + to the SIXPay Saferpay site in order to complete your payment. +{% endtrans %}
{% trans %}First name{% endtrans %}
@@ -8,5 +11,9 @@ Clicking on the {% trans %}Pay Now{% endtrans %} button you wil
{% trans %}Total amount{% endtrans %}
{{ format_currency(amount, currency, locale=session.lang) }}
-
{% trans %}Pay Now{% endtrans %}
+
+ + {%- trans %}Pay Now{% endtrans -%} + +
diff --git a/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html b/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html index a24f431..14c3c31 100644 --- a/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html +++ b/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html @@ -1,6 +1,8 @@ {% extends 'events/payment/transaction_details.html' %} {% block details %} {% if transaction.data.Transaction %} +
{% trans %}Transaction ID{% endtrans %}
+
{{ transaction.data.Transaction.Id }}
{% trans %}Order ID{% endtrans %}
{{ transaction.data.Transaction.OrderId }}
{% endif %} From be945f0b5f044345c545d8fed68b08c7b3ec50ac Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Thu, 29 Jul 2021 11:40:16 +0200 Subject: [PATCH 14/14] Unify usage of Sixpay/Saferpay terms --- payment_sixpay/README.md | 7 ++-- .../indico_payment_sixpay/blueprint.py | 4 +-- .../indico_payment_sixpay/controllers.py | 36 +++++++++---------- .../indico_payment_sixpay/plugin.py | 4 +-- payment_sixpay/indico_payment_sixpay/util.py | 4 +-- payment_sixpay/setup.cfg | 2 +- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/payment_sixpay/README.md b/payment_sixpay/README.md index e84a95a..10703d2 100644 --- a/payment_sixpay/README.md +++ b/payment_sixpay/README.md @@ -1,8 +1,9 @@ -# SIXPay Payment Plugin +# SIXPay-Saferpay Payment Plugin -This plugin provides a SIXPay payment option for Indico's payment module. +This plugin provides a payment option for Indico's payment module using the +SIXPay Saferpay API. -When used, the user will be sent to SIXPay to make the payment, and afterwards +When used, the user will be sent to Saferpay to make the payment, and afterwards they are automatically sent back to Indico. ## Changelog diff --git a/payment_sixpay/indico_payment_sixpay/blueprint.py b/payment_sixpay/indico_payment_sixpay/blueprint.py index 0019b59..d65a91c 100644 --- a/payment_sixpay/indico_payment_sixpay/blueprint.py +++ b/payment_sixpay/indico_payment_sixpay/blueprint.py @@ -7,7 +7,7 @@ from indico.core.plugins import IndicoPluginBlueprint -from indico_payment_sixpay.controllers import (RHInitSixpayPayment, SixPayNotificationHandler, UserCancelHandler, +from indico_payment_sixpay.controllers import (RHInitSixpayPayment, SixpayNotificationHandler, UserCancelHandler, UserFailureHandler, UserSuccessHandler) @@ -20,4 +20,4 @@ blueprint.add_url_rule('/init', 'init', RHInitSixpayPayment, methods=('GET', 'PO blueprint.add_url_rule('/failure', 'failure', UserCancelHandler, methods=('GET', 'POST')) blueprint.add_url_rule('/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST')) blueprint.add_url_rule('/success', 'success', UserSuccessHandler, methods=('GET', 'POST')) -blueprint.add_url_rule('/notify', 'notify', SixPayNotificationHandler, methods=('Get', 'POST')) +blueprint.add_url_rule('/notify', 'notify', SixpayNotificationHandler, methods=('Get', 'POST')) diff --git a/payment_sixpay/indico_payment_sixpay/controllers.py b/payment_sixpay/indico_payment_sixpay/controllers.py index 2beba3f..97fdced 100644 --- a/payment_sixpay/indico_payment_sixpay/controllers.py +++ b/payment_sixpay/indico_payment_sixpay/controllers.py @@ -30,7 +30,7 @@ from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, S class TransactionFailure(Exception): - """A transaction with SixPay failed. + """A transaction with SIXPay failed. :param step: name of the step at which the transaction failed :param details: verbose description of what went wrong @@ -42,12 +42,12 @@ class TransactionFailure(Exception): class RHSixpayBase(RH): - """Request Handler for asynchronous callbacks from SixPay. + """Request Handler for asynchronous callbacks from SIXPay. These handlers are used either by - - the user, when he is redirected from SixPay back to Indico - - SixPay, when it sends back the result of a transaction + - the user, when he is redirected from SIXPay back to Indico + - SIXPay, when it sends back the result of a transaction """ CSRF_ENABLED = False @@ -78,7 +78,7 @@ class RHInitSixpayPayment(RHPaymentBase): } order_description = settings['order_description'].format(**format_map) order_identifier = settings['order_identifier'].format(**format_map) - # see the SixPay Manual + # see the SIXPay Manual # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize # on what these things mean transaction_parameters = { @@ -102,7 +102,7 @@ class RHInitSixpayPayment(RHPaymentBase): 'Abort': url_for_plugin('payment_sixpay.cancel', self.registration.locator.uuid, _external=True) }, 'Notification': { - # where to asynchronously call back from SixPay + # where to asynchronously call back from SIXPay 'NotifyUrl': url_for_plugin('payment_sixpay.notify', self.registration.locator.uuid, _external=True) } } @@ -151,8 +151,8 @@ class RHInitSixpayPayment(RHPaymentBase): return redirect(payment_url) -class SixPayNotificationHandler(RHSixpayBase): - """Handler for notification from SixPay service.""" +class SixpayNotificationHandler(RHSixpayBase): + """Handler for notification from SIXPay service.""" def __init__(self): """Initialize request handler.""" @@ -164,15 +164,15 @@ class SixPayNotificationHandler(RHSixpayBase): self.sixpay_url = get_setting('url') def _process(self): - """Process the reply from SixPay about the transaction.""" + """Process the reply from SIXPay about the transaction.""" try: self._process_confirmation() except TransactionFailure as exc: - SixpayPaymentPlugin.logger.warning('SixPay transaction failed during %s: %s', exc.step, exc.details) + SixpayPaymentPlugin.logger.warning('SIXPay transaction failed during %s: %s', exc.step, exc.details) def _process_confirmation(self): """Process the confirmation response inside indico.""" - # assert transaction status from SixPay + # assert transaction status from SIXPay try: assert_response = self._assert_payment() if self._is_duplicate_transaction(assert_response): @@ -183,15 +183,15 @@ class SixPayNotificationHandler(RHSixpayBase): self._verify_amount(assert_response) self._register_payment(assert_response) except TransactionFailure as exc: - SixpayPaymentPlugin.logger.warning('SixPay transaction failed during %s: %s', exc.step, exc.details) + SixpayPaymentPlugin.logger.warning('SIXPay transaction failed during %s: %s', exc.step, exc.details) raise return True def _perform_request(self, task, endpoint, data): - """Perform a request against SixPay. + """Perform a request against SIXPay. :param task: description of the request, used for error handling - :param endpoint: the URL endpoint *relative* to the SixPay base URL + :param endpoint: the URL endpoint *relative* to the SIXPay base URL :param **data: data passed during the request This will automatically raise any HTTP errors encountered during the @@ -208,7 +208,7 @@ class SixPayNotificationHandler(RHSixpayBase): return response def _assert_payment(self): - """Check the status of the transaction with SixPay. + """Check the status of the transaction with SIXPay. Returns transaction assert data. """ @@ -269,7 +269,7 @@ class SixPayNotificationHandler(RHSixpayBase): return False def _capture_transaction(self, assert_data): - """Confirm to SixPay that the transaction is accepted. + """Confirm to SIXPay that the transaction is accepted. On success returns the response JSON data. """ @@ -343,14 +343,14 @@ class UserFailureHandler(RHSixpayBase): return redirect(url_for('event_registration.display_regform', self.registration.locator.registrant)) -class UserSuccessHandler(SixPayNotificationHandler): +class UserSuccessHandler(SixpayNotificationHandler): """User redirect target in case of successful payment.""" def _process(self): try: self._process_confirmation() except TransactionFailure as exc: - SixpayPaymentPlugin.logger.warning('SixPay transaction failed during %s: %s', exc.step, exc.details) + SixpayPaymentPlugin.logger.warning('SIXPay transaction failed during %s: %s', exc.step, exc.details) flash(_('Your payment could not be confirmed. Please contact the event organizers.'), 'warning') else: flash(_('Your payment has been confirmed.'), 'success') diff --git a/payment_sixpay/indico_payment_sixpay/plugin.py b/payment_sixpay/indico_payment_sixpay/plugin.py index 3f2ec96..a71d62b 100644 --- a/payment_sixpay/indico_payment_sixpay/plugin.py +++ b/payment_sixpay/indico_payment_sixpay/plugin.py @@ -12,7 +12,7 @@ from indico_payment_sixpay.forms import EventSettingsForm, PluginSettingsForm class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): - """SIXPay + """SIXPay Saferpay Provides a payment method using the SIXPay Saferpay API. """ @@ -24,7 +24,7 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): event_settings_form = EventSettingsForm #: global default settings - should be a reasonable default default_settings = { - 'method_name': 'SIXPay', + 'method_name': 'SIXPay Saferpay', 'url': 'https://www.saferpay.com/api/', 'username': None, 'password': None, diff --git a/payment_sixpay/indico_payment_sixpay/util.py b/payment_sixpay/indico_payment_sixpay/util.py index 85b3827..cc8a9d6 100644 --- a/payment_sixpay/indico_payment_sixpay/util.py +++ b/payment_sixpay/indico_payment_sixpay/util.py @@ -36,13 +36,13 @@ def validate_currency(iso_code): """ if iso_code in NON_DECIMAL_CURRENCY: raise HTTPNotImplemented( - _("Unsupported currency '{}' for SixPay. Please contact the organizers").format(iso_code) + _("Unsupported currency '{}' for SIXPay. Please contact the organizers").format(iso_code) ) try: iso4217.Currency(iso_code) except ValueError: raise HTTPNotImplemented( - _("Unknown currency '{}' for SixPay. Please contact the organizers").format(iso_code) + _("Unknown currency '{}' for SIXPay. Please contact the organizers").format(iso_code) ) diff --git a/payment_sixpay/setup.cfg b/payment_sixpay/setup.cfg index 6ee7da7..e2745ec 100644 --- a/payment_sixpay/setup.cfg +++ b/payment_sixpay/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = indico-plugin-payment-sixpay version = 3.0 -description = SIXPay payments for Indico event registration fees +description = SIXPay/Saferpay payments for Indico event registration fees long_description = file: README.md long_description_content_type = text/markdown; charset=UTF-8; variant=GFM url = https://github.com/indico/indico-plugins