orange_payment_api 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.idea/copilot.data.migration.ask2agent.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/orange_payment_api.iml +55 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +121 -0
- data/.overcommit.yml +20 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +227 -0
- data/Rakefile +10 -0
- data/lib/orange_payment_api/client/token.rb +22 -0
- data/lib/orange_payment_api/client.rb +92 -0
- data/lib/orange_payment_api/errors.rb +15 -0
- data/lib/orange_payment_api/request.rb +67 -0
- data/lib/orange_payment_api/transaction/created_info.rb +20 -0
- data/lib/orange_payment_api/transaction/status.rb +35 -0
- data/lib/orange_payment_api/transaction.rb +139 -0
- data/lib/orange_payment_api/version.rb +5 -0
- data/lib/orange_payment_api.rb +20 -0
- data/sig/orange_payment_api/client/token.rbs +18 -0
- data/sig/orange_payment_api/client.rbs +38 -0
- data/sig/orange_payment_api/errors.rbs +21 -0
- data/sig/orange_payment_api/request.rbs +13 -0
- data/sig/orange_payment_api/transaction/created_info.rbs +20 -0
- data/sig/orange_payment_api/transaction/status.rbs +32 -0
- data/sig/orange_payment_api/transaction.rbs +52 -0
- data/sig/orange_payment_api.rbs +8 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dd8a4748b03dfefb0af34aa951483bc6c8e7dfe202cb34a5b18bd2f5bcf6eafd
|
|
4
|
+
data.tar.gz: ddd46e460e2a2369155fb010645fdd3efd38d1c23251169a8680e15019b02cb2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f21e35ed9cc28376796c4fb7974d70df061ab6e97bc3102abe9495544cf21c52762ed88604c6caf3aac27eda2a98ab1fba1071f4df64b3889c7b132c54c31b19
|
|
7
|
+
data.tar.gz: b03d7ed4be8d24a98491ac08739744541a32f9b3aaebfedc3f86c534d018acfa29d91dadc298db7fdb4450c7e148b976ee0c8b57ddb8826424ef1b99115bb0af
|
data/.idea/modules.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/orange_payment_api.iml" filepath="$PROJECT_DIR$/.idea/orange_payment_api.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
|
3
|
+
<component name="ModuleRunConfigurationManager">
|
|
4
|
+
<shared />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="NewModuleRootManager">
|
|
7
|
+
<content url="file://$MODULE_DIR$" />
|
|
8
|
+
<orderEntry type="jdk" jdkName="rbenv: 3.2.2" jdkType="RUBY_SDK" />
|
|
9
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
10
|
+
<orderEntry type="library" scope="PROVIDED" name="addressable (v2.8.8, rbenv: 3.2.2) [gem]" level="application" />
|
|
11
|
+
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, rbenv: 3.2.2) [gem]" level="application" />
|
|
12
|
+
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
13
|
+
<orderEntry type="library" scope="PROVIDED" name="bigdecimal (v4.0.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="bump (v0.10.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
15
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.4.22, rbenv: 3.2.2) [gem]" level="application" />
|
|
16
|
+
<orderEntry type="library" scope="PROVIDED" name="childprocess (v5.1.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
17
|
+
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.6, rbenv: 3.2.2) [gem]" level="application" />
|
|
18
|
+
<orderEntry type="library" scope="PROVIDED" name="crack (v1.0.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
19
|
+
<orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, rbenv: 3.2.2) [gem]" level="application" />
|
|
20
|
+
<orderEntry type="library" scope="TEST" name="faker (v2.22.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
21
|
+
<orderEntry type="library" scope="PROVIDED" name="hashdiff (v1.2.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
22
|
+
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.8, rbenv: 3.2.2) [gem]" level="application" />
|
|
23
|
+
<orderEntry type="library" scope="PROVIDED" name="iniparse (v1.5.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
24
|
+
<orderEntry type="library" scope="PROVIDED" name="json (v2.18.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
25
|
+
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, rbenv: 3.2.2) [gem]" level="application" />
|
|
26
|
+
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
27
|
+
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
28
|
+
<orderEntry type="library" scope="PROVIDED" name="overcommit (v0.68.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
29
|
+
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.27.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
30
|
+
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.10.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
31
|
+
<orderEntry type="library" scope="PROVIDED" name="prism (v1.8.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
32
|
+
<orderEntry type="library" scope="PROVIDED" name="public_suffix (v4.0.7, rbenv: 3.2.2) [gem]" level="application" />
|
|
33
|
+
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
34
|
+
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
35
|
+
<orderEntry type="library" scope="PROVIDED" name="rake (v13.3.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
36
|
+
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.11.3, rbenv: 3.2.2) [gem]" level="application" />
|
|
37
|
+
<orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.4, rbenv: 3.2.2) [gem]" level="application" />
|
|
38
|
+
<orderEntry type="library" scope="TEST" name="rspec (v3.13.2, rbenv: 3.2.2) [gem]" level="application" />
|
|
39
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-core (v3.13.6, rbenv: 3.2.2) [gem]" level="application" />
|
|
40
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-expectations (v3.13.5, rbenv: 3.2.2) [gem]" level="application" />
|
|
41
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.13.7, rbenv: 3.2.2) [gem]" level="application" />
|
|
42
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.13.6, rbenv: 3.2.2) [gem]" level="application" />
|
|
43
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.64.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
44
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
45
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-performance (v1.21.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
46
|
+
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
47
|
+
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.3.2, rbenv: 3.2.2) [gem]" level="application" />
|
|
48
|
+
<orderEntry type="library" scope="TEST" name="standard (v1.37.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
49
|
+
<orderEntry type="library" scope="PROVIDED" name="standard-custom (v1.0.2, rbenv: 3.2.2) [gem]" level="application" />
|
|
50
|
+
<orderEntry type="library" scope="TEST" name="standard-performance (v1.4.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
51
|
+
<orderEntry type="library" scope="TEST" name="timecop (v0.9.10, rbenv: 3.2.2) [gem]" level="application" />
|
|
52
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v2.6.0, rbenv: 3.2.2) [gem]" level="application" />
|
|
53
|
+
<orderEntry type="library" scope="TEST" name="webmock (v3.26.1, rbenv: 3.2.2) [gem]" level="application" />
|
|
54
|
+
</component>
|
|
55
|
+
</module>
|
data/.idea/vcs.xml
ADDED
data/.idea/workspace.xml
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="00235933-b7e5-4761-b108-46473ad20c31" name="Changes" comment="feat: add sandbox option to Client and update token structure">
|
|
8
|
+
<change beforePath="$PROJECT_DIR$/.github/workflows/standard.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.github/workflows/standard.yml" afterDir="false" />
|
|
9
|
+
</list>
|
|
10
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
11
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
12
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
13
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
14
|
+
</component>
|
|
15
|
+
<component name="CopilotPersistence">
|
|
16
|
+
<persistenceIdMap>
|
|
17
|
+
<entry key="_/Users/kassamhousseny/project/orange_payment_api" value="36ebkT3z9iPaZTh49YS5Dv6GdXq" />
|
|
18
|
+
</persistenceIdMap>
|
|
19
|
+
</component>
|
|
20
|
+
<component name="CopilotUserSelectedChatMode">
|
|
21
|
+
<option name="chatModeId" value="Agent" />
|
|
22
|
+
</component>
|
|
23
|
+
<component name="CopilotUserSelectedModel">
|
|
24
|
+
<selectedModels>
|
|
25
|
+
<entry key="chat-panel" value="Gemini 3.1 Pro" />
|
|
26
|
+
<entry key="agent-panel" value="Gemini 3.1 Pro" />
|
|
27
|
+
</selectedModels>
|
|
28
|
+
</component>
|
|
29
|
+
<component name="EmbeddingIndexingInfo">
|
|
30
|
+
<option name="cachedIndexableFilesCount" value="36" />
|
|
31
|
+
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
|
32
|
+
</component>
|
|
33
|
+
<component name="Git.Settings">
|
|
34
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
35
|
+
</component>
|
|
36
|
+
<component name="McpProjectServerCommands">
|
|
37
|
+
<commands />
|
|
38
|
+
<urls />
|
|
39
|
+
</component>
|
|
40
|
+
<component name="ProjectColorInfo">{
|
|
41
|
+
"customColor": "",
|
|
42
|
+
"associatedIndex": 5
|
|
43
|
+
}</component>
|
|
44
|
+
<component name="ProjectId" id="36ebkT3z9iPaZTh49YS5Dv6GdXq" />
|
|
45
|
+
<component name="ProjectViewState">
|
|
46
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
47
|
+
<option name="showLibraryContents" value="true" />
|
|
48
|
+
</component>
|
|
49
|
+
<component name="PropertiesComponent">{
|
|
50
|
+
"keyToString": {
|
|
51
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
52
|
+
"RunOnceActivity.MCP Project settings loaded": "true",
|
|
53
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
54
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
55
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
56
|
+
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
|
57
|
+
"com.intellij.lang.ruby.rbs.tools.collection.workspace.sync.RbsCollectionUpdateProjectActivity#LAST_UPDATE_TIMESTAMP": "1769352231313",
|
|
58
|
+
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
|
59
|
+
"git-widget-placeholder": "main",
|
|
60
|
+
"junie.onboarding.icon.badge.shown": "true",
|
|
61
|
+
"node.js.detected.package.eslint": "true",
|
|
62
|
+
"node.js.detected.package.tslint": "true",
|
|
63
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
64
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
65
|
+
"nodejs_package_manager_path": "npm",
|
|
66
|
+
"ruby.structure.view.model.defaults.configured": "true",
|
|
67
|
+
"settings.editor.selected.configurable": "ruby.rubocop",
|
|
68
|
+
"to.speed.mode.migration.done": "true",
|
|
69
|
+
"vue.rearranger.settings.migration": "true"
|
|
70
|
+
}
|
|
71
|
+
}</component>
|
|
72
|
+
<component name="RubocopSettings">
|
|
73
|
+
<option name="configFilePath" value="/.standard.yml" />
|
|
74
|
+
<option name="useStandard" value="true" />
|
|
75
|
+
</component>
|
|
76
|
+
<component name="SharedIndexes">
|
|
77
|
+
<attachedChunks>
|
|
78
|
+
<set>
|
|
79
|
+
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-RM-253.31033.144" />
|
|
80
|
+
</set>
|
|
81
|
+
</attachedChunks>
|
|
82
|
+
</component>
|
|
83
|
+
<component name="SpringUtil" SPRING_PRE_LOADER_OPTION="true" RAKE_SPRING_PRE_LOADER_OPTION="true" RAILS_SPRING_PRE_LOADER_OPTION="true" />
|
|
84
|
+
<component name="TaskManager">
|
|
85
|
+
<task active="true" id="Default" summary="Default task">
|
|
86
|
+
<changelist id="00235933-b7e5-4761-b108-46473ad20c31" name="Changes" comment="" />
|
|
87
|
+
<created>1765370127421</created>
|
|
88
|
+
<option name="number" value="Default" />
|
|
89
|
+
<option name="presentableId" value="Default" />
|
|
90
|
+
<updated>1765370127421</updated>
|
|
91
|
+
<workItem from="1765370130261" duration="12000" />
|
|
92
|
+
<workItem from="1769352172948" duration="453000" />
|
|
93
|
+
<workItem from="1775731401588" duration="16000" />
|
|
94
|
+
</task>
|
|
95
|
+
<task id="LOCAL-00001" summary="feat: add sandbox option to Client and update token structure">
|
|
96
|
+
<option name="closed" value="true" />
|
|
97
|
+
<created>1775731940800</created>
|
|
98
|
+
<option name="number" value="00001" />
|
|
99
|
+
<option name="presentableId" value="LOCAL-00001" />
|
|
100
|
+
<option name="project" value="LOCAL" />
|
|
101
|
+
<updated>1775731940800</updated>
|
|
102
|
+
</task>
|
|
103
|
+
<option name="localTasksCounter" value="2" />
|
|
104
|
+
<servers />
|
|
105
|
+
</component>
|
|
106
|
+
<component name="VcsManagerConfiguration">
|
|
107
|
+
<MESSAGE value="feat: add sandbox option to Client and update token structure" />
|
|
108
|
+
<option name="LAST_COMMIT_MESSAGE" value="feat: add sandbox option to Client and update token structure" />
|
|
109
|
+
</component>
|
|
110
|
+
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
|
111
|
+
<SUITE FILE_PATH="coverage/orange_payment_api@All_specs_in_spec__orange_payment_api.rcov" NAME="All specs in spec: orange_payment_api Coverage Results" MODIFIED="1775731870219" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="rcov" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" MODULE_NAME="orange_payment_api" />
|
|
112
|
+
</component>
|
|
113
|
+
<component name="github-copilot-workspace">
|
|
114
|
+
<instructionFileLocations>
|
|
115
|
+
<option value=".github/instructions" />
|
|
116
|
+
</instructionFileLocations>
|
|
117
|
+
<promptFileLocations>
|
|
118
|
+
<option value=".github/prompts" />
|
|
119
|
+
</promptFileLocations>
|
|
120
|
+
</component>
|
|
121
|
+
</project>
|
data/.overcommit.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
PreCommit:
|
|
2
|
+
Standard:
|
|
3
|
+
enabled: true
|
|
4
|
+
on_warn: warn
|
|
5
|
+
problem_on_unmodified_line: ignore
|
|
6
|
+
command: ['bundle', 'exec', 'standardrb']
|
|
7
|
+
exclude:
|
|
8
|
+
- 'bin/**/*'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TrailingWhitespace:
|
|
12
|
+
enabled: true
|
|
13
|
+
exclude:
|
|
14
|
+
- '**/db/structure.sql' # Ignore trailing whitespace in generated files
|
|
15
|
+
|
|
16
|
+
PrePush:
|
|
17
|
+
RSpec:
|
|
18
|
+
enabled: true
|
|
19
|
+
required: true
|
|
20
|
+
command: [ 'bundle', 'exec', 'rspec' ]
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"orange_payment_api" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["kassam.housseny@gmail.com"](mailto:"kassam.housseny@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kassam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# OrangePaymentApi
|
|
2
|
+
|
|
3
|
+
<img width="372" height="135" alt="orange_money" src="https://github.com/user-attachments/assets/db4aeda6-ffe8-46c2-aac6-a73e995ec916" />
|
|
4
|
+
|
|
5
|
+
OrangePaymentApi is a Ruby gem that provides a simple way to interact with the [Orange Money Payment API](https://developer.orange.com/apis/orange-money-webpay/).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Install from RubyGems (once published)
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle add orange_payment_api
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install orange_payment_api
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Install from local repository (for testing before publishing)
|
|
24
|
+
|
|
25
|
+
If you want to test this gem in another project without publishing it first, you have several options:
|
|
26
|
+
|
|
27
|
+
#### Option 1: Using a local path in your Gemfile
|
|
28
|
+
|
|
29
|
+
Add this line to your application's Gemfile:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'orange_payment_api', path: '/path/to/orange_payment_api'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then execute:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### Option 2: Using a Git repository
|
|
42
|
+
|
|
43
|
+
If the gem is in a Git repository, add this to your Gemfile:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
gem 'orange_payment_api', git: 'https://github.com/AnTsena/orange_payment_api.git'
|
|
47
|
+
# Or use a specific branch:
|
|
48
|
+
# gem 'orange_payment_api', git: 'https://github.com/AnTsena/orange_payment_api.git', branch: 'main'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then execute:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bundle install
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
#### Option 3: Build and install locally
|
|
58
|
+
|
|
59
|
+
From the orange_payment_api directory, build and install the gem locally:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Build the gem
|
|
63
|
+
gem build orange_payment_api.gemspec
|
|
64
|
+
|
|
65
|
+
# Install the built gem
|
|
66
|
+
gem install ./orange_payment_api-0.1.0.gem
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then in your application's Gemfile:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
gem 'orange_payment_api', '~> 0.1.0'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
Before using the gem, ensure you have the credentials required to interact with the Orange Money API (**Client ID**, **Client Secret**, and **Merchant Key**).
|
|
78
|
+
|
|
79
|
+
### Get Access Token
|
|
80
|
+
|
|
81
|
+
To get an access token, use the `OrangePaymentApi::Client` class and call the `token` method:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
client = OrangePaymentApi::Client.new(client_id: 'your_client_id', client_secret: 'your_client_secret')
|
|
85
|
+
|
|
86
|
+
client.token # <OrangePaymentApi::Client::Token access_token="your_access_token", token_type="Bearer", expires_at=2024-12-30 16:19:42 +0100>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The `token` method stores the data at instance level, so when called multiple times, we don't have to request a new token each time, until the token expires.
|
|
90
|
+
If you want to force a new token, you can use the method:
|
|
91
|
+
```ruby
|
|
92
|
+
client.token!
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Tips
|
|
96
|
+
|
|
97
|
+
We suggest that you store the token in a cache system (like `Redis`, `Memcached`) and pass the value directly
|
|
98
|
+
into the client to avoid requesting a new token each time, across multiple instances of the application:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
token = client.token
|
|
102
|
+
# Store the value in cache
|
|
103
|
+
cache.write('orange_token', token.to_h)
|
|
104
|
+
|
|
105
|
+
# then next time, you can use the value from cache
|
|
106
|
+
token = cache.read('orange_token')
|
|
107
|
+
|
|
108
|
+
client = OrangePaymentApi::Client.new(client_id: 'your_client_id', client_secret: 'your_client_secret', token: token)
|
|
109
|
+
|
|
110
|
+
# If the token is still valid, the client will use it directly without requesting a new one.
|
|
111
|
+
# If it's expired, the client will automatically request a new one to make a request.
|
|
112
|
+
client.token
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Initialize a Payment
|
|
116
|
+
|
|
117
|
+
To initialize a payment, use the `OrangePaymentApi::Transaction` class and call the `initiate_payment!` method:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
ENV["ORANGE_PAYMENT_API_CLIENT_ID"] = "CLIENT_ID"
|
|
121
|
+
ENV["ORANGE_PAYMENT_API_CLIENT_SECRET"] = "CLIENT_SECRET"
|
|
122
|
+
|
|
123
|
+
merchant_key = "your_merchant_key"
|
|
124
|
+
client = OrangePaymentApi::Client.new(sandbox: true)
|
|
125
|
+
|
|
126
|
+
transaction = OrangePaymentApi::Transaction.new(client: client, sandbox: true)
|
|
127
|
+
# OR (if you want to use the default client)
|
|
128
|
+
# transaction = OrangePaymentApi::Transaction.new(sandbox: true)
|
|
129
|
+
|
|
130
|
+
order_id = SecureRandom.hex(10)
|
|
131
|
+
|
|
132
|
+
# Initiate a payment
|
|
133
|
+
response = transaction.initiate_payment!(
|
|
134
|
+
amount: 1000.0,
|
|
135
|
+
merchant_key: merchant_key,
|
|
136
|
+
order_id: order_id,
|
|
137
|
+
currency: "MGA",
|
|
138
|
+
return_url: "http://www.merchant-example.org/return",
|
|
139
|
+
cancel_url: "http://www.merchant-example.org/cancel",
|
|
140
|
+
notif_url: "http://www.merchant-example.org/notif"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
pp response
|
|
144
|
+
# => #<struct OrangePaymentApi::Transaction::CreatedInfo
|
|
145
|
+
# status=201,
|
|
146
|
+
# message="OK",
|
|
147
|
+
# pay_token="v1pszsvkuxjmdzodwafycf833nxccmt8dviiuoyya79md4fzm4sryo10q6v0pdx7",
|
|
148
|
+
# payment_url="https://mpayment.orange-money.com/sx/mpayment/abstract/v1pszsvkuxjmdzodwafycf833nxccmt8dviiuoyya79md4fzm4sryo10q6v0pdx7",
|
|
149
|
+
# notif_token="0pxpzs95hi1ndzsxcbfjycq7i70dfcyz">
|
|
150
|
+
|
|
151
|
+
# Redirect the user to response.payment_url to complete the payment
|
|
152
|
+
|
|
153
|
+
# Get the status of the payment
|
|
154
|
+
status_response = transaction.get_status(
|
|
155
|
+
order_id: order_id,
|
|
156
|
+
amount: 1000.0,
|
|
157
|
+
pay_token: response.pay_token
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
pp status_response
|
|
161
|
+
# => #<struct OrangePaymentApi::Transaction::Status
|
|
162
|
+
# status="SUCCESS",
|
|
163
|
+
# order_id="your_order_id",
|
|
164
|
+
# txnid="MP241210.1234.A12345">
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
|
|
169
|
+
<summary>Transaction Status and CreatedInfo Structures</summary>
|
|
170
|
+
|
|
171
|
+
The `OrangePaymentApi::Transaction::CreatedInfo` and `OrangePaymentApi::Transaction::Status` classes provide detailed information about transactions and their statuses.
|
|
172
|
+
|
|
173
|
+
### OrangePaymentApi::Transaction::CreatedInfo
|
|
174
|
+
|
|
175
|
+
The `OrangePaymentApi::Transaction::CreatedInfo` class provides information about a newly created payment transaction.
|
|
176
|
+
|
|
177
|
+
It includes the following attributes:
|
|
178
|
+
|
|
179
|
+
- `status`: HTTP status code (always 201 for successful transaction creation).
|
|
180
|
+
- `message`: A message from the API about the transaction creation (always "OK" for successful creations).
|
|
181
|
+
- `pay_token`: A token used to check the payment status later.
|
|
182
|
+
- `payment_url`: The URL where the payment can be completed. Redirect users to this URL.
|
|
183
|
+
- `notif_token`: A token used for notification verification. Store this on your server to verify notifications from Orange.
|
|
184
|
+
|
|
185
|
+
### OrangePaymentApi::Transaction::Status
|
|
186
|
+
|
|
187
|
+
The `OrangePaymentApi::Transaction::Status` class provides information about the status of a payment transaction.
|
|
188
|
+
|
|
189
|
+
It includes the following attributes:
|
|
190
|
+
|
|
191
|
+
- `status`: The status of the transaction. Possible values:
|
|
192
|
+
- `"NOT FOUND"`: All parameters of the request don't match an existing transaction.
|
|
193
|
+
- `"INITIATED"`: The transaction has been created but payment not yet completed.
|
|
194
|
+
- `"PENDING"`: The payment is being processed.
|
|
195
|
+
- `"EXPIRED"`: The transaction has expired.
|
|
196
|
+
- `"SUCCESS"`: The payment was completed successfully.
|
|
197
|
+
- `"FAILED"`: The payment failed.
|
|
198
|
+
- `order_id`: The unique identifier of the order associated with the transaction.
|
|
199
|
+
- `txnid`: The unique transaction identifier provided by Orange (also accessible via `transaction_id` method).
|
|
200
|
+
|
|
201
|
+
The class also provides predicate methods for checking status:
|
|
202
|
+
- `not_found?`: Returns true if status is "NOT FOUND"
|
|
203
|
+
- `initiated?`: Returns true if status is "INITIATED"
|
|
204
|
+
- `pending?`: Returns true if status is "PENDING"
|
|
205
|
+
- `expired?`: Returns true if status is "EXPIRED"
|
|
206
|
+
- `success?`: Returns true if status is "SUCCESS"
|
|
207
|
+
- `failed?`: Returns true if status is "FAILED"
|
|
208
|
+
|
|
209
|
+
</details>
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
214
|
+
|
|
215
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
216
|
+
|
|
217
|
+
## Contributing
|
|
218
|
+
|
|
219
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/orange_payment_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/orange_payment_api/blob/main/CODE_OF_CONDUCT.md).
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
224
|
+
|
|
225
|
+
## Code of Conduct
|
|
226
|
+
|
|
227
|
+
Everyone interacting in the OrangePaymentApi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/orange_payment_api/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Client
|
|
5
|
+
Token = Struct.new(:access_token, :token_type, :scope, :expires_at, keyword_init: true) do
|
|
6
|
+
def expired?
|
|
7
|
+
Time.now >= expires_at - expires_margin
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def valid?
|
|
11
|
+
!expired?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# Add a margin to the expiration time to avoid using an expired token.
|
|
17
|
+
def expires_margin
|
|
18
|
+
5 * 60 # 5 minutes
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client/token"
|
|
4
|
+
require_relative "request"
|
|
5
|
+
|
|
6
|
+
module OrangePaymentApi
|
|
7
|
+
class Client
|
|
8
|
+
include Request
|
|
9
|
+
|
|
10
|
+
ACCESS_TOKEN_URL = "https://api.orange.com/oauth/v3/token"
|
|
11
|
+
|
|
12
|
+
LANGUAGES = {
|
|
13
|
+
fr: "FR",
|
|
14
|
+
mg: "MG"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :client_id, :client_secret, :language, :sandbox
|
|
18
|
+
|
|
19
|
+
# @param client_id [String] the client id provided by Orange for the application.
|
|
20
|
+
# Defaults to the value of the ORANGE_PAYMENT_API_CLIENT_ID environment variable.
|
|
21
|
+
# @param client_secret [String] the client secret provided by MVola for the application.
|
|
22
|
+
# Defaults to the value of the ORANGE_PAYMENT_API_CLIENT_SECRET environment variable.
|
|
23
|
+
# @param token [Hash, Token] a previously stored token to use for the client.
|
|
24
|
+
# If provided and that it is still valid, it will be used instead of fetching a new one.
|
|
25
|
+
def initialize(client_id: ENV["ORANGE_PAYMENT_API_CLIENT_ID"],
|
|
26
|
+
client_secret: ENV["ORANGE_PAYMENT_API_CLIENT_SECRET"],
|
|
27
|
+
language: LANGUAGES[:fr],
|
|
28
|
+
token: nil,
|
|
29
|
+
sandbox: false)
|
|
30
|
+
raise ArgumentError, "Client ID and Client Secret are required" if client_id.nil? || client_secret.nil?
|
|
31
|
+
|
|
32
|
+
# Warn an invalid user language if not one of the supported languages
|
|
33
|
+
unless LANGUAGES.key?(language.downcase.to_sym)
|
|
34
|
+
logger.warn "Invalid user language: #{language}. Using default language: #{LANGUAGES[:fr]}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@client_id = client_id
|
|
38
|
+
@client_secret = client_secret
|
|
39
|
+
@sandbox = sandbox
|
|
40
|
+
|
|
41
|
+
@token = build_token_from(token)
|
|
42
|
+
@language = LANGUAGES.fetch(language.downcase.to_sym, LANGUAGES[:fr])
|
|
43
|
+
@mutex = Mutex.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get the token. If the token is not valid, it will be refreshed.
|
|
47
|
+
def token
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
return @token if @token&.valid?
|
|
50
|
+
|
|
51
|
+
@token = build_token_from(fetch_token)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Force the token to be refreshed, even if it is still valid.
|
|
56
|
+
def token!
|
|
57
|
+
@token = nil
|
|
58
|
+
token
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# This method is used to build a token object from a hash or a token object.
|
|
64
|
+
def build_token_from(data)
|
|
65
|
+
return data if data.is_a?(Token)
|
|
66
|
+
return unless data.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
hash = data.dup
|
|
69
|
+
hash[:expires_at] ||= Time.now + hash.delete(:expires_in).to_i
|
|
70
|
+
hash[:expires_at] = Time.parse(hash[:expires_at].to_s) if hash[:expires_at].is_a?(String)
|
|
71
|
+
Token.new(**hash.slice(:access_token, :token_type, :expires_at, :scope))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def headers
|
|
75
|
+
{
|
|
76
|
+
authorization: "Basic #{Base64.strict_encode64("#{@client_id}:#{@client_secret}")}",
|
|
77
|
+
accept: "application/json"
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Perform a request to fetch a new token from the MVola API.
|
|
82
|
+
def fetch_token
|
|
83
|
+
body = {
|
|
84
|
+
grant_type: "client_credentials"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
response = post(ACCESS_TOKEN_URL, headers: headers, body: body)
|
|
88
|
+
|
|
89
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class BadRequestError < Error; end
|
|
7
|
+
|
|
8
|
+
class UnauthorizedError < Error; end
|
|
9
|
+
|
|
10
|
+
class NotFoundError < Error; end
|
|
11
|
+
|
|
12
|
+
class ServerError < Error; end
|
|
13
|
+
|
|
14
|
+
class ApiError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
|
|
6
|
+
module OrangePaymentApi
|
|
7
|
+
module Request
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators OrangePaymentApi, :logger
|
|
11
|
+
|
|
12
|
+
# Method to perform a POST request
|
|
13
|
+
# @param url [String] the URL to perform the request
|
|
14
|
+
# @param args [Hash] the arguments to pass to the request
|
|
15
|
+
# @option args [Hash] :params the parameters to pass to the request
|
|
16
|
+
# @option args [Hash] :headers the headers to pass to the request
|
|
17
|
+
# @option args [Hash] :body the body to pass to the request
|
|
18
|
+
# @option args [Hash] :json the JSON to pass to the request body. Replace the body if present
|
|
19
|
+
def post(url, args = {})
|
|
20
|
+
uri = URI.parse(url)
|
|
21
|
+
headers = args.delete(:headers) || {}
|
|
22
|
+
headers["Content-Type"] = "application/json" if args.key?(:json)
|
|
23
|
+
|
|
24
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
25
|
+
|
|
26
|
+
request.form_data = args[:body] if args[:body]
|
|
27
|
+
if (json = args.delete(:json))
|
|
28
|
+
request.body = json.to_json
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts("POST #{url} #{args}")
|
|
32
|
+
response = build_http(uri, args).request(request)
|
|
33
|
+
puts("Response: #{response.code} #{response.body.inspect}")
|
|
34
|
+
|
|
35
|
+
handle_error(response)
|
|
36
|
+
|
|
37
|
+
response
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def build_http(uri, args)
|
|
43
|
+
uri.query = URI.encode_www_form(args[:params]) if args[:params]
|
|
44
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
45
|
+
http.use_ssl = true
|
|
46
|
+
|
|
47
|
+
http
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def handle_error(response)
|
|
51
|
+
case response.code.to_i
|
|
52
|
+
when 200..299
|
|
53
|
+
# Do nothing
|
|
54
|
+
when 400
|
|
55
|
+
raise OrangePaymentApi::BadRequestError, response.body
|
|
56
|
+
when 401
|
|
57
|
+
raise OrangePaymentApi::UnauthorizedError, response.body
|
|
58
|
+
when 404
|
|
59
|
+
raise OrangePaymentApi::NotFoundError, response.body
|
|
60
|
+
when 500..599
|
|
61
|
+
raise OrangePaymentApi::ServerError, response.body
|
|
62
|
+
else
|
|
63
|
+
raise OrangePaymentApi::ApiError, response.body
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Transaction
|
|
5
|
+
# Struct to hold information about a created transaction.
|
|
6
|
+
# This is aimed to be used internally and not exposed to the public API.
|
|
7
|
+
# Status is always `201` returned by the API when a transaction is created successfully.
|
|
8
|
+
# Attributes:
|
|
9
|
+
# - status: HTTP status code (always 201 for created transactions)
|
|
10
|
+
# - message: A message from the API about the transaction creation. Always "OK" for successful creations.
|
|
11
|
+
# - pay_token: A token used to proceed with the payment.
|
|
12
|
+
# - payment_url: The URL where the payment can be completed. Clients should redirect users to this URL.
|
|
13
|
+
# - notif_token: A token used for notification purposes. You would want to store this in your server, so you can verify notifications from Orange.
|
|
14
|
+
CreatedInfo = Struct.new(:status, :message, :pay_token, :payment_url, :notif_token, keyword_init: true) do
|
|
15
|
+
def created?
|
|
16
|
+
status == 201
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Transaction
|
|
5
|
+
NOT_FOUND = "NOT FOUND"
|
|
6
|
+
INITIATED = "INITIATED"
|
|
7
|
+
PENDING = "PENDING"
|
|
8
|
+
EXPIRED = "EXPIRED"
|
|
9
|
+
SUCCESS = "SUCCESS"
|
|
10
|
+
FAILED = "FAILED"
|
|
11
|
+
|
|
12
|
+
# Response's object of the transaction status
|
|
13
|
+
# Attributes:
|
|
14
|
+
# - status: Status of the transaction.
|
|
15
|
+
# Possible values: "NOT FOUND", "INITIATED", "PENDING", "EXPIRED", "SUCCESS", "FAILED".
|
|
16
|
+
# The status 'NOT FOUND' is returned when all parameters of the request don't match an existing transaction.
|
|
17
|
+
# - order_id: The unique identifier of the order associated with the transaction.
|
|
18
|
+
# - txnid: The unique identifier of the transaction provided by Orange.
|
|
19
|
+
Status = Struct.new(:status, :order_id, :txnid, keyword_init: true) do
|
|
20
|
+
# Define predicate methods for each possible status
|
|
21
|
+
# e.g., initiated?, pending?, success?, etc.
|
|
22
|
+
# Returns true if the transaction status matches the method name.
|
|
23
|
+
[NOT_FOUND, INITIATED, PENDING, EXPIRED, SUCCESS, FAILED].each do |s|
|
|
24
|
+
define_method("#{s.downcase.tr(" ", "_")}?") do
|
|
25
|
+
status == s
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# This field may be empty depending on the status.
|
|
30
|
+
def transaction_id
|
|
31
|
+
txnid
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client"
|
|
4
|
+
require_relative "transaction/status"
|
|
5
|
+
require_relative "transaction/created_info"
|
|
6
|
+
|
|
7
|
+
module OrangePaymentApi
|
|
8
|
+
class Transaction
|
|
9
|
+
include Request
|
|
10
|
+
|
|
11
|
+
API_VERSION = "v1"
|
|
12
|
+
BASE_URL = "https://api.orange.com/"
|
|
13
|
+
DEV_ENDPOINT_URL = "#{BASE_URL}orange-money-webpay/dev/#{API_VERSION}"
|
|
14
|
+
PROD_ENDPOINT = "#{BASE_URL}orange-money-webpay/#{API_VERSION}"
|
|
15
|
+
DEFAULT_CURRENCY = "MGA"
|
|
16
|
+
DEV_CURRENCY = "OUV"
|
|
17
|
+
|
|
18
|
+
attr_reader :client, :endpoint
|
|
19
|
+
|
|
20
|
+
def initialize(client: nil)
|
|
21
|
+
@client = client || Client.new
|
|
22
|
+
@endpoint = @client.sandbox ? DEV_ENDPOINT_URL : PROD_ENDPOINT
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Initiate a payment transaction.
|
|
26
|
+
# @param amount [Integer] The amount to be paid in the currency.
|
|
27
|
+
# @param merchant_key [String] The merchant key provided by Orange.
|
|
28
|
+
# @param order_id [String] The unique order ID for the transaction. up to 30 chars alphanumeric.
|
|
29
|
+
# @param return_url [String] The URL to redirect the user after payment completion.
|
|
30
|
+
# @param cancel_url [String] The URL to redirect the user if the payment is canceled.
|
|
31
|
+
# @param notif_url [String] The URL for server-to-server notification of payment status.
|
|
32
|
+
# @param currency [String] The currency code for the transaction. Default to MGA (Malagasy Ariary).
|
|
33
|
+
# @param language [String] The language code for the transaction. Default to the client instance language.
|
|
34
|
+
# @param reference [String, nil] An optional reference string for the transaction.
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# {
|
|
38
|
+
# "merchant_key": "a86b2087",
|
|
39
|
+
# "order_id": "MY_ORDER_ID_08082105_0023457",
|
|
40
|
+
# "currency": "Ar",
|
|
41
|
+
# "amount": 1200,
|
|
42
|
+
# "return_url": "http://myvirtualshop.webnode.es",
|
|
43
|
+
# "cancel_url": "http://myvirtualshop.webnode.es/txncncld/",
|
|
44
|
+
# "notif_url": "http://www.merchant-example2.org/notif",
|
|
45
|
+
# "lang": "fr"
|
|
46
|
+
# "reference": "ref Merchant"
|
|
47
|
+
# }
|
|
48
|
+
def initiate_payment!(amount:,
|
|
49
|
+
merchant_key:,
|
|
50
|
+
order_id:,
|
|
51
|
+
currency: DEFAULT_CURRENCY,
|
|
52
|
+
return_url:,
|
|
53
|
+
cancel_url:,
|
|
54
|
+
notif_url:,
|
|
55
|
+
language: client.language,
|
|
56
|
+
reference: nil)
|
|
57
|
+
ensure_valid_order_id!(order_id)
|
|
58
|
+
ensure_valid_url!(return_url)
|
|
59
|
+
ensure_valid_url!(cancel_url)
|
|
60
|
+
ensure_valid_url!(notif_url)
|
|
61
|
+
|
|
62
|
+
request_currency = client.sandbox ? DEV_CURRENCY : currency
|
|
63
|
+
|
|
64
|
+
payload = {
|
|
65
|
+
amount: amount.to_f.round(2),
|
|
66
|
+
currency: request_currency,
|
|
67
|
+
merchant_key: merchant_key,
|
|
68
|
+
order_id: order_id,
|
|
69
|
+
return_url: return_url,
|
|
70
|
+
cancel_url: cancel_url,
|
|
71
|
+
notif_url: notif_url,
|
|
72
|
+
lang: language,
|
|
73
|
+
reference: reference
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
url = url_for("webpayment")
|
|
77
|
+
logger.debug "Initiating payment with payload: #{payload}"
|
|
78
|
+
response = post(url, json: payload, headers: headers)
|
|
79
|
+
logger.debug "Payment initiated. Response: #{response.body}"
|
|
80
|
+
|
|
81
|
+
parsed_body = JSON.parse(response.body, symbolize_names: true)
|
|
82
|
+
CreatedInfo.new(**parsed_body.slice(:status, :message, :pay_token, :payment_url, :notif_token))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get the status of a payment using the server correlation ID.
|
|
86
|
+
# This can be used to poll the status of a payment.
|
|
87
|
+
def get_status(order_id:, amount:, pay_token:)
|
|
88
|
+
url = url_for("transactionstatus")
|
|
89
|
+
|
|
90
|
+
payload = {
|
|
91
|
+
order_id: order_id,
|
|
92
|
+
amount: amount.to_f.round(2),
|
|
93
|
+
pay_token: pay_token
|
|
94
|
+
}
|
|
95
|
+
response = post(url, json: payload, headers: headers)
|
|
96
|
+
|
|
97
|
+
parsed_body = JSON.parse(response.body, symbolize_names: true)
|
|
98
|
+
Status.new(**parsed_body.slice(:status, :order_id, :txnid))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Generate the URL for the given path by joining the base URL and the endpoint.
|
|
104
|
+
def url_for(path = "")
|
|
105
|
+
safe_path = path.gsub(/^\//, "") # Remove the starting slash from the path (if any) to avoid incorrect URL generation
|
|
106
|
+
Pathname.new(endpoint).join(safe_path).to_s
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def headers
|
|
110
|
+
{
|
|
111
|
+
"Authorization" => "Bearer #{client.token.access_token}",
|
|
112
|
+
"Accept" => "application/json",
|
|
113
|
+
"Content-Type" => "application/json",
|
|
114
|
+
"Cache-Control" => "no-cache"
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ensure_valid_order_id!(order_id)
|
|
119
|
+
raise ArgumentError, "order_id is required" if order_id.nil?
|
|
120
|
+
raise ArgumentError, "order_id must be 30 characters long maximum" if order_id.size > 30
|
|
121
|
+
|
|
122
|
+
order_id_regexp = /\A[\w\-\.]+\z/
|
|
123
|
+
|
|
124
|
+
unless order_id.match?(order_id_regexp)
|
|
125
|
+
raise ArgumentError, "order_id contains invalid characters. " \
|
|
126
|
+
"Allowed characters are alphanumeric, hyphen (-), underscore (_), dot (.), space and comma (,)."
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ensure_valid_url!(url)
|
|
131
|
+
uri = URI.parse(url)
|
|
132
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
133
|
+
raise ArgumentError, "Invalid URL format: #{url}"
|
|
134
|
+
end
|
|
135
|
+
rescue URI::InvalidURIError
|
|
136
|
+
raise ArgumentError, "Invalid URL format: #{url}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "time"
|
|
6
|
+
require "json"
|
|
7
|
+
require_relative "orange_payment_api/version"
|
|
8
|
+
require_relative "orange_payment_api/errors"
|
|
9
|
+
require_relative "orange_payment_api/client"
|
|
10
|
+
require_relative "orange_payment_api/transaction"
|
|
11
|
+
|
|
12
|
+
module OrangePaymentApi
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Set up a default logger
|
|
18
|
+
self.logger = Logger.new($stdout) # Logs to the console
|
|
19
|
+
logger.level = Logger::INFO # Default log level
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module OrangePaymentApi
|
|
2
|
+
class Client
|
|
3
|
+
class Token
|
|
4
|
+
attr_accessor access_token: String
|
|
5
|
+
attr_accessor token_type: String
|
|
6
|
+
attr_accessor scope: String
|
|
7
|
+
attr_accessor expires_at: Time
|
|
8
|
+
|
|
9
|
+
def initialize: (access_token: String, token_type: String, expires_at: Time) -> void
|
|
10
|
+
def expired?: () -> bool
|
|
11
|
+
def valid?: () -> bool
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def expires_margin: () -> Integer
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module OrangePaymentApi
|
|
2
|
+
class Client
|
|
3
|
+
include Request
|
|
4
|
+
|
|
5
|
+
ACCESS_TOKEN_URL: String
|
|
6
|
+
LANGUAGES: HashWithIndifferentAccess[Symbol, String]
|
|
7
|
+
|
|
8
|
+
@client_id: String
|
|
9
|
+
@client_secret: String
|
|
10
|
+
@language: Symbol
|
|
11
|
+
@token: Token?
|
|
12
|
+
@sandbox: bool
|
|
13
|
+
@mutex: Mutex
|
|
14
|
+
|
|
15
|
+
attr_reader client_id: String
|
|
16
|
+
attr_reader client_secret: String
|
|
17
|
+
attr_reader language: Symbol | String
|
|
18
|
+
attr_reader sandbox: bool
|
|
19
|
+
|
|
20
|
+
def initialize: (?client_id: String,
|
|
21
|
+
?client_secret: String,
|
|
22
|
+
?language: Symbol | String,
|
|
23
|
+
?token: Token?,
|
|
24
|
+
?sandbox: bool) -> void
|
|
25
|
+
|
|
26
|
+
def token: () -> Token
|
|
27
|
+
|
|
28
|
+
def token!: () -> Token
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_token_from: (token_data: Hash[Symbol, String]) -> Token
|
|
33
|
+
|
|
34
|
+
def headers: () -> Hash[String, String]
|
|
35
|
+
|
|
36
|
+
def fetch_token: () -> Hash[Symbol, String]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class BadRequestError < Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class UnauthorizedError < Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class NotFoundError < Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ServerError < Error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class ApiError < Error
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module OrangePaymentApi
|
|
2
|
+
module Request
|
|
3
|
+
def get: (String url, ?::Hash[untyped, untyped] args) -> Net::HTTPResponse
|
|
4
|
+
|
|
5
|
+
def post: (String url, ?::Hash[untyped, untyped] args) -> Net::HTTPResponse
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_http: (URI uri, Hash[untyped, untyped] args) -> Net::HTTP
|
|
10
|
+
|
|
11
|
+
def handle_error: (Net::HTTPResponse response) -> void
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Transaction
|
|
5
|
+
class CreatedInfo
|
|
6
|
+
attr_accessor status: String
|
|
7
|
+
attr_accessor message: String
|
|
8
|
+
attr_accessor pay_token: String
|
|
9
|
+
attr_accessor payment_url: String
|
|
10
|
+
attr_accessor notif_token: String
|
|
11
|
+
|
|
12
|
+
def initialize: (
|
|
13
|
+
status: String,
|
|
14
|
+
message: String,
|
|
15
|
+
pay_token: String,
|
|
16
|
+
payment_url: String,
|
|
17
|
+
notif_token: String) -> void
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Transaction
|
|
5
|
+
class Status
|
|
6
|
+
attr_accessor status: String
|
|
7
|
+
attr_accessor order_id: String
|
|
8
|
+
attr_accessor txnid: String
|
|
9
|
+
|
|
10
|
+
NOT_FOUND: String
|
|
11
|
+
INITIATED: String
|
|
12
|
+
PENDING: String
|
|
13
|
+
EXPIRED: String
|
|
14
|
+
SUCCESS: String
|
|
15
|
+
FAILED: String
|
|
16
|
+
|
|
17
|
+
def initialize: (
|
|
18
|
+
status: String,
|
|
19
|
+
order_id: String,
|
|
20
|
+
txnid: String
|
|
21
|
+
) -> void
|
|
22
|
+
|
|
23
|
+
def not_found?: () -> bool
|
|
24
|
+
def initiated?: () -> bool
|
|
25
|
+
def pending?: () -> bool
|
|
26
|
+
def expired?: () -> bool
|
|
27
|
+
def success?: () -> bool
|
|
28
|
+
def failed?: () -> bool
|
|
29
|
+
def transaction_id: () -> String
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OrangePaymentApi
|
|
4
|
+
class Transaction
|
|
5
|
+
include Request
|
|
6
|
+
|
|
7
|
+
API_VERSION: String
|
|
8
|
+
BASE_URL: String
|
|
9
|
+
DEV_ENDPOINT_URL: String
|
|
10
|
+
PROD_ENDPOINT: String
|
|
11
|
+
DEFAULT_CURRENCY: String
|
|
12
|
+
DEV_CURRENCY: String
|
|
13
|
+
|
|
14
|
+
@client: Client
|
|
15
|
+
@endpoint: String
|
|
16
|
+
@sandbox: bool
|
|
17
|
+
|
|
18
|
+
attr_reader client: Client
|
|
19
|
+
attr_reader endpoint: String
|
|
20
|
+
attr_reader sandbox: bool
|
|
21
|
+
|
|
22
|
+
def initialize: (?client: Client?, ?sandbox: bool) -> void
|
|
23
|
+
|
|
24
|
+
def initiate_payment!: (
|
|
25
|
+
amount: Float | Integer,
|
|
26
|
+
merchant_key: String,
|
|
27
|
+
order_id: String,
|
|
28
|
+
return_url: String,
|
|
29
|
+
cancel_url: String,
|
|
30
|
+
notif_url: String,
|
|
31
|
+
?currency: String,
|
|
32
|
+
?language: String,
|
|
33
|
+
?reference: String?
|
|
34
|
+
) -> CreatedInfo
|
|
35
|
+
|
|
36
|
+
def get_status: (
|
|
37
|
+
order_id: String,
|
|
38
|
+
amount: Float | Integer,
|
|
39
|
+
pay_token: String
|
|
40
|
+
) -> Status
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def url_for: (?String path) -> String
|
|
45
|
+
|
|
46
|
+
def headers: () -> Hash[String, String]
|
|
47
|
+
|
|
48
|
+
def ensure_valid_order_id!: (String order_id) -> void
|
|
49
|
+
|
|
50
|
+
def ensure_valid_url!: (String url) -> void
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: orange_payment_api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kassam
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A Ruby client for the Orange Payment API
|
|
13
|
+
email:
|
|
14
|
+
- kassam.housseny@antsena.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- ".idea/copilot.data.migration.ask2agent.xml"
|
|
20
|
+
- ".idea/modules.xml"
|
|
21
|
+
- ".idea/orange_payment_api.iml"
|
|
22
|
+
- ".idea/vcs.xml"
|
|
23
|
+
- ".idea/workspace.xml"
|
|
24
|
+
- ".overcommit.yml"
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- CODE_OF_CONDUCT.md
|
|
27
|
+
- LICENSE.txt
|
|
28
|
+
- README.md
|
|
29
|
+
- Rakefile
|
|
30
|
+
- lib/orange_payment_api.rb
|
|
31
|
+
- lib/orange_payment_api/client.rb
|
|
32
|
+
- lib/orange_payment_api/client/token.rb
|
|
33
|
+
- lib/orange_payment_api/errors.rb
|
|
34
|
+
- lib/orange_payment_api/request.rb
|
|
35
|
+
- lib/orange_payment_api/transaction.rb
|
|
36
|
+
- lib/orange_payment_api/transaction/created_info.rb
|
|
37
|
+
- lib/orange_payment_api/transaction/status.rb
|
|
38
|
+
- lib/orange_payment_api/version.rb
|
|
39
|
+
- sig/orange_payment_api.rbs
|
|
40
|
+
- sig/orange_payment_api/client.rbs
|
|
41
|
+
- sig/orange_payment_api/client/token.rbs
|
|
42
|
+
- sig/orange_payment_api/errors.rbs
|
|
43
|
+
- sig/orange_payment_api/request.rbs
|
|
44
|
+
- sig/orange_payment_api/transaction.rbs
|
|
45
|
+
- sig/orange_payment_api/transaction/created_info.rbs
|
|
46
|
+
- sig/orange_payment_api/transaction/status.rbs
|
|
47
|
+
homepage: https://github.com/AnTsena/orange_payment_api
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/AnTsena/orange_payment_api
|
|
52
|
+
source_code_uri: https://github.com/AnTsena/orange_payment_api
|
|
53
|
+
changelog_uri: https://github.com/AnTsena/orange_payment_api/CHANGELOG.md
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 2.7.0
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 4.0.6
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: A Ruby client for the Orange Payment API
|
|
71
|
+
test_files: []
|