Compare commits

...

195 Commits

Author SHA1 Message Date
5c4c0e1b7b Merge pull request 'staging' (#5) from staging into master
Reviewed-on: #5
2024-10-22 18:42:28 +02:00
63bbbdf1e1 Update events to trigger actions lint-test.yaml 2024-10-22 18:42:28 +02:00
283485068c Merge pull request 'staging' (#3) from staging into master
All checks were successful
Lint and Test / lint (push) Successful in 10m22s
Lint and Test / test (push) Successful in 10m5s
Reviewed-on: #3
2024-10-22 16:37:21 +02:00
fb37ee85f2 Update package name 2024-10-22 16:37:21 +02:00
f840b7aac7
Add omitempty to JSON tags to CardBillingAddress AddressLine2 and AdminArea2
All checks were successful
Lint and Test / lint (push) Successful in 10m22s
Lint and Test / test (push) Successful in 10m6s
2024-10-22 14:24:40 +02:00
6217511bbd
Add omitempty to JSON tags to PaymentSourceCard type 2024-10-22 14:23:56 +02:00
27ffa97190
Add vendor directory to .gitignore 2024-10-22 14:22:47 +02:00
Alex Pliutau
0a5a40c12c
Create CONTRIBUTING.md
Some checks failed
Lint and Test / test (push) Waiting to run
Lint and Test / lint (push) Has been cancelled
2024-09-24 11:51:26 +02:00
Alex Pliutau
c9c1a38641 Remove unused tokenservice endpoints 2024-08-30 13:57:08 +02:00
Suhail Gupta
9451befb2b
Update README.md (#273) 2024-08-30 13:46:26 +02:00
Penny
daabe58f88
fix: Log request body (#272) 2024-08-19 13:24:58 +02:00
Alex Pliutau
5052fd4286 Fix #271 2024-07-10 10:57:51 +02:00
Alex Pliutau
3713084b64 go 1.21 2024-06-03 10:17:12 +02:00
Alex Pliutau
bfdf8e6e97 #263 2024-06-03 10:09:31 +02:00
Alex Pliutau
6ebb0352bc Remove deprecated endpoints 2024-06-03 09:53:55 +02:00
Alex Pliutau
e5db87f4a3 #260: rework SubscriptionBase struct 2024-05-27 12:44:13 +02:00
Seyed Hossein Hosseini Motlagh
cd3344edd1
Adds the ToDuration function to the paypalExpirationTime type. This function allows easy conversion of the internal value to a time.Duration type, which is crucial when using paypalExpirationTime values with Redis expiration times. (#265) 2024-05-27 12:36:56 +02:00
Alex Pliutau
b8f2c8c573 Use github actions 2024-05-27 12:36:25 +02:00
Penny
5fddf59f71
feat: get access token automatically (#261) 2024-02-01 10:07:34 +01:00
Albert Putra Purnama
26473c3630
Fixes test case (#259) 2023-10-04 22:31:48 +02:00
ihipop
ebf596676e
bring back landing_page option since commit #d355a65df01285c6aa4003099f8bfcb1a48ab517 (#251) 2022-11-18 15:34:38 +01:00
Akshay Prabhakant
4c16ffad0a
#177 feature/Invoice: init, added Invoice related structs, implemented met… (#249)
* feature/Invoice: init, added Invoice related structs, implemented methods:GenerateInvoiceNumber,GetInvoiceDetails(by ID)

* feature/Invoice: added implemented endpoints to README along with Usage

Co-authored-by: Akshay Prabhakant <akshayprabhakant@Akshays-MacBook-Pro.local>
2022-10-28 09:20:28 +02:00
Akshay Prabhakant
1bb626d559
Feature/private mutex (#248)
* feat/mutexPrivate:init; trying whether changes get pushed

* feat/privatedMutex: privated the mutex as a field in Client struct, added tests for this private mutex field

* feature/privateMutex: commented the composition of sync.Mutex

* feature/privateMutex: resolved issues with user credentials

Co-authored-by: Akshay Prabhakant <akshayprabhakant@Akshays-MacBook-Pro.local>
2022-10-20 09:11:49 +02:00
felipe.fuerback
f0575ee562
Refactor/code improvements (#247)
* Code improvements

* Revert log validation improvement
2022-10-07 22:07:27 +02:00
Binit kumar singh
defe3e09a4
Fix authorize order to return the correct response (#243) 2022-07-22 10:48:14 +02:00
Christopher Lorke
efc9bbdee0
Update types.go (#244)
Add missing field
2022-07-22 10:47:53 +02:00
Naoki Ikeguchi
b7d3cfd093
feat: Add missing fields into ErrorResponseDetail (#242)
* feat: Add missing fields into ErrorResponseDetail

* test: Add the example response as an unit test
2022-07-11 22:13:11 -07:00
Konnor Klashinsky
d94b350a31
add sale_id field to Refund struct (#238) 2022-06-18 22:04:53 +02:00
Konnor Klashinsky
48f8ee15ce
add payer_id field to Subscriber struct (#239) 2022-06-18 22:04:38 +02:00
Andy
7dc1f997d6
fix update order patchrequest (#240)
Co-authored-by: andy <xxxandyfenxxx@xxx.com>
2022-06-18 22:04:14 +02:00
maxzhao-bolt
c9ae5c4190
Adding PaymentMethod support into ApplicationContext (#237) 2022-03-17 11:33:06 -07:00
bartek1912
5c5393bd47
feat: add missing plan override (#229) 2022-03-05 08:16:50 +01:00
Florian Kinder
d381642422
Added PayPal-Mock-Response for capture (#232) 2022-03-05 08:16:25 +01:00
Anooj Muttavarapu
ca6845e257
Fixes updaterequest (#236)
* Fixes updaterequest

Fixes update request to support the patch request body format. https://developer.paypal.com/api/orders/v2/#definition-patch

* Change return type

Return type should only be error.

* Add nil return

Add nil return
2022-03-05 08:15:53 +01:00
uwevil
fc3ffe5b60
fix typing close #234 (#235) 2022-02-25 09:08:14 +01:00
Alex Pliutau
bf9103c70e
Merge pull request #231 from madprogramer/master
Potential fix for Issue 230
2021-11-11 12:48:32 +01:00
Ahmet Akkoç
bf0c207b52
Added spaces
`} else {`
2021-11-10 23:33:48 +01:00
Ahmet Akkoç
28f2333770
Fixed else syntax 2021-11-08 10:21:28 +01:00
Ahmet Akkoç
d815f6d8d9 Potential fix for Issue 230 2021-11-07 20:37:12 +01:00
Alex Pliutau
1a2c109908
Merge pull request #227 from emead/master
Add status field to CaptureAmount
2021-10-05 13:32:59 +02:00
Edwin Mead
50acd8fce1 Add status field to CaptureAmount 2021-10-04 15:01:28 +01:00
Nik Bisht
64bfbc9b80
Update ReAuthorization call parameters (#226) 2021-09-27 20:34:39 -07:00
Alex Pliutau
6fb262a2b0 License 2021 2021-09-17 08:45:25 +02:00
Alex Pliutau
7599a2d162 #201: add SUBSCRIBE_NOW 2021-09-17 08:44:39 +02:00
Alex Pliutau
ef48df5471
Merge pull request #223 from Yuusuke8686/fix_#219
Fix Reauthorize authorized payment parameter #219
2021-09-17 08:41:32 +02:00
Alex Pliutau
5a5daa8596
Merge pull request #224 from TariqueNasrullah/master
Grammatical error and Typo fixed in comments
2021-09-17 08:41:10 +02:00
Alex Pliutau
cbabac29c8
Merge pull request #225 from RensTillmann/patch-1
Added missing `items` on `PurchaseUnit` struct
2021-09-17 08:40:48 +02:00
Rens Tillmann
e6a47fe5e7
Added missing items on PurchaseUnit struct
When you call `GetOrder()` paypal also returns the items for `purchase_unit` ref: https://developer.paypal.com/docs/api/orders/v2/#definition-purchase_unit
The `PurchaseUnit` struct was missing this parameter.
2021-09-17 00:56:14 +02:00
TariqueNasrullah
59e3c9f3b0 fixed typo 2021-09-13 00:54:22 +06:00
TariqueNasrullah
326d69cc9e fixed grammatical error 2021-09-13 00:53:49 +06:00
Yuusuke Miyatake
4ab23e5f78 #219: fix parameter 2021-08-25 00:05:57 +09:00
rohanr-bolt
06067823c6
Add cancel billing agreement function (#222) 2021-08-19 10:29:00 -07:00
kenbolt
d5cba40cd6
Adding partner referral response (#217)
* Adding links to refund response

* Adding partner referral response
2021-07-27 15:56:29 -07:00
Cameron Jarnot
c2074af736
Update payment source type for optional fields (#216)
* updated payment source fields

* revert

* update payment source type
2021-07-27 11:59:36 -07:00
Dario Nevistić
297b4fa2c6
Updating Order with missing fields (#215)
* Update Order struct with API /v2 response fields according to the documentation https://developer.paypal.com/docs/api/orders/v2/\#orders_get

* Add order completed unmarshal response  unit test
2021-07-27 16:37:20 +02:00
kenbolt
8e5d5220ab
Adding links to refund response (#213) 2021-07-26 16:13:23 -07:00
Cameron Jarnot
2d088532de
Re-adding billing agreements endpoint (#212)
* adding billing_agreements.go file

* added new types

* added tests

* added notice of deprecation
2021-07-21 07:53:38 +02:00
Cameron Jarnot
f195993596
deletions made (#209) 2021-07-20 08:37:55 +02:00
Thomas Frössman
d2210adf9c
Convert depracation comments to godoc and deprecate Resource struct. (#207)
* Fix deprecation comment formatting for godoc.

* Deprecate Resource struct
2021-07-20 08:37:23 +02:00
Gerasimovich Igor
7f2eec9b56
Added missing BillingAgreementId key in webhook (#205)
* Added missing BillingAgreementId key in webhook

Webhook resource contains `billing_agreement_id`. Exists in type: `PAYMENT.SALE.COMPLETED` when a subscription is paid.

* review fix
2021-07-14 09:08:11 +02:00
Cameron Jarnot
392c0e9d69
Add create billing agreement from token (#204)
* Add create billing agreement from token
2021-07-13 10:14:56 -07:00
Cameron Jarnot
a3977a8e74
Adding functions for requesting billing agreement token (#203)
* created billing agreements file

* CreateBillingAgreementToken created with new types

* updated formatting

* error to return nil

* removed new line

* added test
2021-07-12 11:25:19 +02:00
Alex Pliutau
021cc68201
Merge pull request #199 from thomasf/remove-invalid-events
remove invalid event structs
2021-04-30 08:07:26 +02:00
Thomas Frössman
bbd66c6089 remove invalid event structs 2021-04-29 22:11:13 +02:00
Alex Pliutau
41099f6b6e fix typo in PaymentInstruction 2021-04-10 18:17:20 +02:00
Alex Pliutau
ed294ef317 #160: add PAY_NOW user action 2021-04-10 18:16:14 +02:00
Alex Pliutau
d90cb75e47 #163: payment_instruction on the purchase_unit_request 2021-04-10 18:13:45 +02:00
Alex Pliutau
62598e5880 #188: GetCapturedPaymentDetails 2021-04-10 18:09:05 +02:00
Alex Pliutau
52acc61786 #192: refactor Event type 2021-04-10 17:56:52 +02:00
Alex Pliutau
1e23f8dd7d return examples 2021-04-10 17:49:27 +02:00
Alex Pliutau
36281c1526
Merge pull request #196 from erkin97/fix-omitempty
Fix omitempty struct tag on Product.Description
2021-04-10 17:41:09 +02:00
Alex Pliutau
0a85944fee
Merge pull request #194 from Yuusuke8686/fix_#188
Add capture detail interface
2021-04-10 17:40:44 +02:00
Erkin Matkaziev
f694e414cd
Fix omitempty struct tag on Product.Description 2021-02-19 19:28:59 +09:00
Yuusuke
a3c2eaa0d4 add capture detail interface 2021-02-07 00:56:29 +09:00
Yash Suresh Chandra
a5cff3c18c
idempotent create order api (#191) 2021-01-25 12:52:19 -08:00
Alex Pliutau
df9918548b update readme 2021-01-20 10:30:37 +01:00
Alex Pliutau
f807b7d046
Merge pull request #190 from thomasf/context
add context
2021-01-20 10:27:33 +01:00
Thomas Frössman
3b835ea26a deprecate a few inconsistently named things and but keep aliases. 2021-01-20 09:31:54 +01:00
Thomas Frössman
e83fd911e0 add GetWebhookEventTypes 2021-01-03 21:20:12 +01:00
Thomas Frössman
cc8b3cee69 add context support 2021-01-03 10:49:14 +01:00
Dimas Ragil T
0f9d1cca16
Add Revise Subscription (#187)
* Add function revise subscription

* Update README add revise subscription
2020-12-16 10:40:27 +01:00
方航
98cae62470
fix param error while query transactions of subscriptions (#186)
url needs start_time and end_time, and both fields remove timezome
2020-11-28 09:56:24 +01:00
方航
ef386ff32f
fix param error (#185)
fix param error
2020-11-26 13:35:49 +01:00
jrapoport
476102bb76
Order should include payments & purchase_units (#179)
* Order should include purchase_units

* PayerWithNameAndPhone needs Address for Order

* add SellerProtection to CaptureAmount

* add RecipientType(s)

* add RecipientType(s)

* BatchHeader.BatchStatus values
2020-11-17 10:59:42 +01:00
Jason Hord
13112c66e5
Adds SellerReceivableBreakdown and CustomID fields to the Resource type (#180)
* Adds SellerReceivableBreakdown and CustomID fields to the Resource type

* Adds EventCheckoutOrderApproved constant and Intent, PurchaseUnits, Payer fields to Resource type
2020-11-17 10:59:19 +01:00
Jason Hord
5508bfebaf
Adds EventCheckoutOrderApproved constant and Intent, PurchaseUnits, Payer fields to Resource type (#181) 2020-11-17 10:59:02 +01:00
Jason Hord
ced90e676f
Adds a CustomID field to the SubscriptionBase struct (#182) 2020-11-17 10:58:44 +01:00
Jason Hord
abd51d2823
Changes omitempty tagging on Product struct to match Paypal API requirements (#183) 2020-11-17 10:58:07 +01:00
yunuseskisan
d1e5575c35
Add support for SellerReceivableBreakdown (#178)
* Add support for SellerReceivableBreakdown

https://developer.paypal.com/docs/api/orders/v2/#orders_capture

* Update types.go

* Update types.go
2020-10-23 09:52:58 +02:00
John Landis
289b2669ef
Order payer information (#169)
* simplest change

* re-using existing type

Co-authored-by: John Landis <jlandis@bcorporation.net>
2020-09-16 15:30:17 -07:00
Moo
761c20a4ed
change SharedListResponse TotalItems, TotalPages from string to int; Error: json: cannot unmarshal number into Go struct field ListSubscriptionPlansResponse.total_items of type string (#166)
Co-authored-by: Moo <mri@robin-data.io>
2020-08-25 09:20:11 -07:00
Onezino Moreira
18b47a0652
add recipient_wallet payout field (#165) 2020-08-13 16:20:35 -07:00
Rami
a0d03ecb0d
Fixes response from CreateSubscription (#155) 2020-06-01 11:02:18 -07:00
Alex Pliutau
905bf2eaf2 remove workflows 2020-05-31 18:18:05 +02:00
Alex Pliutau
a678ccb4b1
Remove examples, keep godoc as source of truth (#154) 2020-05-31 17:02:56 +02:00
Alex Pliutau
e84c6d06c5 Remove confusing message 2020-05-31 16:57:11 +02:00
Alex Pliutau
2e16e4fb10 auto-release workflow 2020-05-31 16:56:11 +02:00
Rami
0461b35d07
Fixed incorrect response type in ListSubscriptionPlans (#153)
* Update billing plan

* Update billing plan

* Update billing plan

* Kickstart product creation

* Added product api

* Added list product api and refactored some similar structs

* Reverted endpoint for old billing plans

* Added subscription plans create get update list

* Added subscription plans activate/deactivate/update pricing scheme

* Added integration tests for subscription plans

* go fmt

* Add subscriptions API

* Removed AWS library added by mistake

* reverted billing plans url

* resolved MR discussions

* Fixed but in listing subscription plans, and some constants

* Fixed wrong type for subscription plan description

* Fixed updating issue

Co-authored-by: rami <admin@okitoo.net>
2020-05-31 16:49:02 +02:00
Rami
d355a65df0
Subscriptions, Subscription plans and Products API (#151) 2020-05-30 22:14:19 -07:00
Roopak Venkatakrishnan
b3eb2c69dd
Revert "Some updates to refund endpoint (#121)" (#147)
This reverts commit efe72c1ed4.
2020-05-04 13:17:53 -07:00
Eric Lee
efe72c1ed4
Some updates to refund endpoint (#121)
* s/authorization/authorizations in url path

* Update authorization object to the paypal v2 version

* duplicate field

* rename some fields

* Update types for refund endpoint

* actually, don't do a breaking change

* .

* pointer

Co-authored-by: Alex Pliutau <a.pliutau@gmail.com>
2020-05-02 17:09:58 +02:00
Alex Pliutau
fc9dcf749b Fix v2 endpoints, migrate to v1 2020-05-02 17:00:45 +02:00
Jakub Horák
4f66415fcd
Add transaction search /v1/reporting/transactions (#145) 2020-05-02 16:43:30 +02:00
Bülent Rahim Kazancı
21b349dbdc
Add functions of create, get, update, delete and list webhooks (#146)
* Add webhook creation function

* Add webhook list function

* Add webhook delete function

* Add webhook get function

* Add webhook update function

* Update documentation related to webhooks

* Fix addressed issues on code review
2020-05-02 16:42:37 +02:00
Jakub Horák
497963d8a5
Fix typo (#144) 2020-04-24 12:05:22 +02:00
kenbolt
7827c1418f
Auth capture idempotency and refund idempotency (#143)
* Idempotency for capture auth
2020-04-21 09:23:44 -07:00
kenbolt
907cf40be1
Adding a capture auth idempotent function (#142)
* Adding a capture auth idempotent function

* refactoring

* better name

Co-Authored-By: Roopak Venkatakrishnan <roopak.v@gmail.com>

* what's in a name

Co-authored-by: Roopak Venkatakrishnan <roopak.v@gmail.com>
2020-04-20 12:49:30 -07:00
Leaf
1fb8e0b1fa
Add order status consts (#140)
Fixes #136
2020-04-18 23:37:18 -07:00
kenbolt
2091d46d04
Refund from capture endpoint function (#141)
* Capture refund API call

* .
2020-04-08 09:51:17 -07:00
Alessandro Sechi
6f0fe4d870
Added GetSubscriptionDetails method (#135)
* Issue #133

Added GetSubscriptionDetails method

* Issue #133
2020-03-27 12:05:56 +01:00
Sergio Segrera
8686a619a4
Add capture response address field (#137) 2020-03-27 12:05:43 +01:00
Alex Pliutau
0e7098bda4 Include the 'items' in the response when capturing orders 2020-01-06 15:27:03 +01:00
Maksymilian Lewicki
89088bee4e Update endpoints in coverage section in README (#132) 2020-01-06 17:21:21 +03:00
Eric Lee
5822d5ee58 Print error details in addition to message and http code (#131)
* Print error details in addition to message and http code

* %v -> %+v
2019-12-16 16:34:48 -08:00
Eric Lee
16c52d39ba Update item struct to match paypal model (#129)
* Update item struct to match paypal model

* test
2019-11-20 22:12:36 -08:00
Eric Lee
5feda2cf5e Update payment capture response type to include links (#128) 2019-11-18 14:44:42 -08:00
chaseadams509
a6e6c7ae98 Add partner overrides to referral request (#124)
* Add partner overrides to referral request

Add this object:
https://developer.paypal.com/docs/api/partner-referrals/v2/#definition-partner_configuration_override

* Update types.go
2019-10-11 11:54:25 -07:00
chaseadams509
bffc96851f Add types to support Partner Referral API (#123)
* Add types to support Partner Referral API

Add in the type structs to allow a POST call to v2/customer/partner-referrals
It sends a ReferralRequest, and returns a response of type Resource with only Links populated
https://developer.paypal.com/docs/api/partner-referrals/v2/

* Pointer for optional field

* Update types.go

Co-Authored-By: Roopak Venkatakrishnan <roopak.v@gmail.com>
2019-10-10 14:36:25 -07:00
chaseadams509
c865af7932 Add Webhook data types (#120)
* Add Webhook data types

Add the Event data types for payment capture and merchant onboarding events, which are sent for PayPal Commerce Platform integrations.

* remove unnecessary empty line

* name change and points for omitempty objects

* Update types.go

Co-Authored-By: Roopak Venkatakrishnan <roopak.v@gmail.com>
2019-09-25 13:44:33 -07:00
Eric Lee
c4226ce43c Fix authorizations (#119)
Fix authorizations
2019-09-24 12:34:56 -07:00
Roopak Venkatakrishnan
06a298ef76
Reset after reading (#118) 2019-09-24 07:51:55 -07:00
Roopak Venkatakrishnan
cd72fd3aec
Add verify webhook signature response (#117) 2019-09-19 08:03:42 -07:00
Alex Pliutau
8e3811377b
Merge pull request #115 from plutov/roopakv/fix_auth_capture
Fix payment capture API
2019-09-07 10:01:39 +02:00
Roopak Venkatakrishnan
fee7108fae
Fix payment capture 2019-09-06 08:00:21 -07:00
Alvaro Carvajal
c0ae3c0d06 Add custom_id to captured orders (#114) 2019-09-03 13:39:22 -07:00
Roopak Venkatakrishnan
32920d672b
Example for go modules in Readme (#111) 2019-08-27 09:09:46 -07:00
Roopak Venkatakrishnan
dc6329e927
Use v3 in tests to prevent indirect dependencies (#112) 2019-08-27 09:09:30 -07:00
Roopak Venkatakrishnan
b200e0a324 Go Modules support (#110)
* Go Modules support

* move into same package

* Update billing_test.go

* Revert example_test.go and remove billing_test.go

* Keep 1.11 and 1.12 in travis
2019-08-27 12:38:42 +02:00
Alex Pliutau
55f369d8c8 Fix billing_test.go 2019-08-21 15:53:29 +02:00
Alex Pliutau
e70e544c1a Update package name 2019-08-21 15:50:20 +02:00
Alex Pliutau
f7f8c60772 Remove sourcegraph button 2019-08-21 13:19:25 +02:00
Alex Pliutau
035df40512 Fix go lint 2019-08-21 13:16:54 +02:00
Alex Pliutau
6f5da20f7c Change import path to github.com/plutov/PayPal-Go-SDK 2019-08-21 13:11:13 +02:00
Alex Pliutau
1ac6304458
Merge pull request #107 from roopakv/roopakv/fix_authorize
Add more type definitions & fix capture order
2019-08-21 09:39:18 +02:00
Roopak Venkatakrishnan
167f2d58ab
Add more type definitions 2019-08-20 23:24:18 -07:00
Alex Pliutau
fb6ff6be3d Fix go rerpotr card issues 2019-08-19 15:33:46 +02:00
Alex Pliutau
61f8ec387c #105: Add override_merchant_preferences to BillingAgreement 2019-08-12 16:09:45 +02:00
Alex Pliutau
458e08c529
Merge pull request #104 from chriskolenko/master
Add fields to ApplicationContext
2019-08-06 17:39:02 +02:00
Chris Kolenko
188f7c26fc
Add fields to ApplicationContext
* ReturnURL
* CancelURL
2019-08-07 01:28:11 +10:00
Alex Pliutau
ce57fa2d8b
Merge pull request #103 from dennisstritzke/master
Adding item category and shipping preference.
2019-08-05 16:19:08 +02:00
Dennis Stritzke
cce5baf4a0 Adding item category and shipping preference. 2019-08-05 16:12:34 +02:00
Alex Pliutau
37b06bc520
Merge pull request #101 from dennisstritzke/master
Adding support for PayPal Order to contain Items
2019-08-02 12:54:44 +02:00
Dennis Stritzke
04ab7f83a0 Adding support for PayPal Order to contain the order breakdown and Items to contain UnitAmounts. 2019-08-02 12:17:49 +02:00
Alex Pliutau
224bd1f949 #89: Add UpdateOrder 2019-07-30 15:12:58 +02:00
Alex Pliutau
637e1c4b15 Add contributors 2019-07-22 15:03:15 +02:00
Alex Pliutau
aeec25d551 Add contributors 2019-07-22 15:02:18 +02:00
Alex Pliutau
fd64790cd2
Merge pull request #100 from roopakv/roopakv/update_co
Use create order payer for create order
2019-07-22 14:56:14 +02:00
Alex Pliutau
0862ea5ecc
Merge pull request #90 from logpacker/payment-source
#89: V2: Add payment-source type
2019-07-22 14:56:01 +02:00
Roopak Venkatakrishnan
1daa788741
Use create order payer 2019-07-22 00:23:07 -07:00
Alex Pliutau
586d690e8e
Merge pull request #99 from roopakv/roopakv/fix_address
Update shipping address to reflect paypal API
2019-07-22 09:03:44 +02:00
Roopak Venkatakrishnan
ed0c8f85a0
fix payer for orders 2019-07-21 18:29:26 -07:00
Roopak Venkatakrishnan
80ccf89d37
Update shipping address to reflect paypal API 2019-07-21 12:46:13 -07:00
Alex Pliutau
6891f8d2f0 Improve integratin tests 2019-07-21 09:49:15 +02:00
Alex Pliutau
3f993bc542 Add payment-source type 2019-07-21 09:28:07 +02:00
Alex Pliutau
da794934b8
Merge pull request #96 from roopakv/roopakv/fix_app_ctxt
Fix app context
2019-07-19 08:54:22 +02:00
Roopak Venkatakrishnan
265abe72b2
Fix app context 2019-07-18 22:21:31 -07:00
Alex Pliutau
888360be1f
Merge pull request #93 from roopakv/roopakv/tiny_fix
[Tiny fix] Create Order
2019-06-29 10:24:53 +02:00
Roopak Venkatakrishnan
e6cc18e840
Merge branch 'master' into roopakv/tiny_fix 2019-06-28 21:01:15 -07:00
Roopak Venkatakrishnan
a329399695 more fixes 2019-06-28 21:00:29 -07:00
Roopak Venkatakrishnan
da7d8507ff [Tiny fix] Create Order 2019-06-28 19:08:24 -07:00
Alex Pliutau
42de9cae2e
Merge pull request #92 from roopakv/roopakv/use_correct_payee
Use correct payee for PurchaseUnitRequest
2019-06-28 09:05:17 +02:00
Roopak Venkatakrishnan
f51b1fe7a0 fixes 2019-06-27 11:39:07 -07:00
Roopak Venkatakrishnan
dd82be9889 use specific payee 2019-06-27 10:55:21 -07:00
Alex Pliutau
973f502217
Merge pull request #91 from roopakv/roopakv/create_order
Create Order API Call
2019-06-27 08:54:10 +02:00
Roopak Venkatakrishnan
c85effd4a6 Add to readme 2019-06-26 21:22:30 -07:00
Roopak Venkatakrishnan
fd873cc35b Create Order API Call 2019-06-26 21:16:21 -07:00
Alex Pliutau
ebc7df5708
Merge pull request #87 from danielmltn/master
code style efficiencies added
2019-06-17 13:53:31 +02:00
Daniel Melton
cc05027dd1 revert of time.Until which fails on 1.5,1.6, & 1.7 2019-06-17 07:39:03 -04:00
Daniel Melton
2c6b470ed7 code style efficiencies added 2019-06-15 22:39:08 -04:00
Alex Pliutau
17ba889ca0
Merge pull request #84 from tungquach/master
Update order api endpoints, remove unsupported order do void api
2019-04-22 20:58:05 +02:00
tungquach
561690178b update order struct, add purchase units 2019-04-21 11:43:53 +07:00
tungquach
4557d3e5a7 update order struct 2019-04-21 10:44:07 +07:00
tungquach
3adfc8c315 update order intergration test 2019-04-21 10:13:01 +07:00
tungquach
1f766901f9 Update order api endpoints 2019-04-21 10:08:48 +07:00
Alex Pliutau
0c3090ecb8 Update year in LICENSE.md 2019-03-27 18:13:18 +01:00
Alex Pliutau
4d9251997c Payments V2 2019-03-27 09:27:53 +01:00
Alex Pliutau
5af7837061 Add v1/v2 note 2019-03-26 15:39:18 +01:00
Alex Pliutau
114f3ab4be
Merge pull request #77 from apourchet/apourchet/fix-race-sendwithauth
Locking SendWithAuth to fix race condition
2019-03-01 10:49:21 +01:00
Alex Pliutau
6f2ea3a580
Merge pull request #80 from CodeLingoBot/rewrite
Fix function comments based on best practices from Effective Go
2019-03-01 10:48:30 +01:00
Antoine Pourchet
4303b79440 Added comments for clarity 2019-02-28 20:17:03 -08:00
CodeLingo Bot
560c513235 Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-02-28 21:52:50 +00:00
Antoine Pourchet
bb976e776e Locking SendWithAuth to fix race condition
When SendWithAuth gets called in a multithreaded environment on the
same Client object, a concurrent read and write of c.Token might happen
in GetAccessToken.
This patch solves the issue by locking the client while we get a new
access token in SendWithAuth.
2019-02-26 23:26:37 -08:00
Alex Pliutau
7a8d9e5531
Merge pull request #75 from meitarim/master
added ListBillingPlans
2019-01-14 10:17:11 +01:00
meitar
c23e130afd added ListBillingPlans 2019-01-14 07:06:05 +02:00
Alex Pliutau
3896aaa167
Merge pull request #73 from lindroth/master
added GetPaymentsWithFilter and Filter to list filtered payments
2018-12-16 14:07:16 +01:00
Alex Pliutau
e31da31dcc
Merge pull request #70 from cristaloleg/master
Get rid of escape characters
2018-12-16 14:04:53 +01:00
Alex Pliutau
9c283e9d42 #69: add payment response 2018-12-16 13:26:41 +01:00
Alex Pliutau
f17191367d Fix UTs 2018-12-16 13:24:11 +01:00
Alex Pliutau
2eacad2d5d #71 Quantity field of Item struct type must change from string to uint32 type 2018-12-16 13:06:54 +01:00
Rickard Lindroth
d5a45e7e9a
Update README.md
wrong parameter name for listing payments
2018-12-10 14:18:00 +01:00
Rickard Lindroth
36f64e9a3b added GetPaymentsWithFilter and Filter to list filtered payments 2018-12-10 12:54:15 +01:00
Oleg Kovalov
f04460162e Get rid of escape characters 2018-10-13 23:45:06 +02:00
Alex Pliutau
f7201fdd87
Merge pull request #68 from logpacker/fix-quantity-type
#66: fix item.quantity type
2018-10-11 16:11:22 +07:00
Alex Pliutau
2148226300
Merge pull request #67 from logpacker/go-111
Add go 1.11 to travis
2018-10-10 20:51:36 +07:00
Alex Pliutau
caff1125ce Add go 1.11 to travis 2018-10-10 20:47:17 +07:00
36 changed files with 3386 additions and 1786 deletions

4
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,4 @@
#### What does this PR do?
#### Where should the reviewer start?
#### How should this be manually tested?
#### Any background context you want to provide?

50
.github/workflows/lint-test.yaml vendored Normal file
View File

@ -0,0 +1,50 @@
name: Lint and Test
on:
push:
branches:
- features/*
- testing/*
- fix/*
- enhance/*
pull_request:
branches:
- master
- staging
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Install dependencies
run: go get .
- name: Install linters
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
go install mvdan.cc/unparam@latest
- name: go vet
run: go vet ${{ inputs.path }}
- name: staticcheck
run: staticcheck ${{ inputs.path }}
- name: unparam
run: unparam ${{ inputs.path }}
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Install dependencies
run: go get .
- name: Run Tests
run: go test -v -race ./...

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
.vscode
vendor/

View File

@ -1,12 +0,0 @@
language: go
go:
- 1.5
- 1.6
- 1.7
- 1.8
- 1.9
- 1.10
install:
- export PATH=$PATH:$HOME/gopath/bin
script:
- go test -v -race

10
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,10 @@
First off all, thank you for considering contributing to this project. It's people like you that make it such a great tool.
Keep an open mind! Improving documentation, bug triaging, or writing tutorials are all examples of helpful contributions that mean less work for you.
Some basic suggestions to get you started:
- Make sure the PR is up-to-date with the latest changes in the main branch.
- Make sure the PR passes all the tests.
- Make sure the PR passes the linter.
- Make sure the PR is well documented and formatted.
- Make sure the PR is well tested.

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2017 Aliaksandr Pliutau
Copyright (c) 2024 Aliaksandr Pliutau
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

279
README.md
View File

@ -1,130 +1,31 @@
[![Go Report Card](https://goreportcard.com/badge/logpacker/PayPal-Go-SDK)](https://goreportcard.com/report/logpacker/PayPal-Go-SDK)
[![Build Status](https://travis-ci.org/logpacker/PayPal-Go-SDK.svg?branch=master)](https://travis-ci.org/logpacker/PayPal-Go-SDK)
[![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/logpacker/PayPal-Go-SDK)
[![Chat at https://gitter.im/logpacker/PayPal-Go-SDK](https://img.shields.io/badge/gitter-dev_chat-46bc99.svg)](https://gitter.im/logpacker/PayPal-Go-SDK?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sourcegraph](https://sourcegraph.com/github.com/logpacker/PayPal-Go-SDK/-/badge.svg)](https://sourcegraph.com/github.com/logpacker/PayPal-Go-SDK?badge)
[Docs](https://pkg.go.dev/github.com/plutov/paypal)
### GO client for PayPal REST API
<p>
<a href="https://github.com/plutov/paypal/releases"><img src="https://img.shields.io/github/release/plutov/paypal.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/plutov/paypal?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
</p>
### Coverage
* POST /v1/oauth2/token
* POST /v1/payments/payment
* GET /v1/payments/payment/**ID**
* GET /v1/payments/payment
* GET /v1/payments/authorization/**ID**
* POST /v1/payments/authorization/**ID**/capture
* POST /v1/payments/authorization/**ID**/void
* POST /v1/payments/authorization/**ID**/reauthorize
* GET /v1/payments/sale/**ID**
* POST /v1/payments/sale/**ID**/refund
* GET /v1/payments/refund/**ID**
* GET /v1/payments/orders/**ID**
* POST /v1/payments/orders/**ID**/authorize
* POST /v1/payments/orders/**ID**/capture
* POST /v1/payments/orders/**ID**/do-void
* POST /v1/identity/openidconnect/tokenservice
* GET /v1/identity/openidconnect/userinfo/?schema=**SCHEMA**
* POST /v1/payments/payouts
* GET /v1/payments/payouts/**ID**
* GET /v1/payments/payouts-item/**ID**
* POST /v1/payments/payouts-item/**ID**/cancel
* GET /v1/payment-experience/web-profiles
* POST /v1/payment-experience/web-profiles
* GET /v1/payment-experience/web-profiles/**ID**
* PUT /v1/payment-experience/web-profiles/**ID**
* DELETE /v1/payment-experience/web-profiles/**ID**
* POST /v1/vault/credit-cards
* DELETE /v1/vault/credit-cards/**ID**
* PATCH /v1/vault/credit-cards/**ID**
* GET /v1/vault/credit-cards/**ID**
* GET /v1/vault/credit-cards
* POST /v1/payments/billing-plans
* PATCH /v1/payments/billing-plans/***ID***
* POST /v1/payments/billing-agreements
* POST /v1/payments/billing-agreements/***TOKEN***/agreement-execute
# Go client for PayPal REST API
### Missing endpoints
It is possible that some endpoints are missing in this SDK Client, but you can use built-in **paypalsdk** functions to perform a request: **NewClient -> NewRequest -> SendWithAuth**
## Paypal REST API Docs
### New Client
[Get started with PayPal REST APIs](https://developer.paypal.com/api/rest/)
## Missing endpoints
It is possible that some endpoints are missing in this client, but you can use built-in `paypal` functions to perform a request: `NewClient -> NewRequest -> SendWithAuth`
## Usage
```go
import "github.com/logpacker/PayPal-Go-SDK"
import "github.com/plutov/paypal/v4"
// Create a client instance
c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox)
c, err := paypal.NewClient("clientID", "secretID", paypal.APIBaseSandBox)
c.SetLog(os.Stdout) // Set log to terminal stdout
accessToken, err := c.GetAccessToken()
```
### Create direct paypal payment
```go
amount := paypalsdk.Amount{
Total: "7.00",
Currency: "USD",
}
redirectURI := "http://example.com/redirect-uri"
cancelURI := "http://example.com/cancel-uri"
description := "Description for this payment"
paymentResult, err := c.CreateDirectPaypalPayment(amount, redirectURI, cancelURI, description)
```
### Create custom payment
```go
p := paypalsdk.Payment{
Intent: "sale",
Payer: &paypalsdk.Payer{
PaymentMethod: "credit_card",
FundingInstruments: []paypalsdk.FundingInstrument{paypalsdk.FundingInstrument{
CreditCard: &paypalsdk.CreditCard{
Number: "4111111111111111",
Type: "visa",
ExpireMonth: "11",
ExpireYear: "2020",
CVV2: "777",
FirstName: "John",
LastName: "Doe",
},
}},
},
Transactions: []paypalsdk.Transaction{paypalsdk.Transaction{
Amount: &paypalsdk.Amount{
Currency: "USD",
Total: "7.00",
},
Description: "My Payment",
}},
RedirectURLs: &paypalsdk.RedirectURLs{
ReturnURL: "http://...",
CancelURL: "http://...",
},
}
paymentResponse, err := client.CreatePayment(p)
```
### Execute approved payment
```go
paymentID := "PAY-17S8410768582940NKEE66EQ"
payerID := "7E7MGXCWTTKK2"
executeResult, err := c.ExecuteApprovedPayment(paymentID, payerID)
```
### Get payment by ID
```go
payment, err := c.GetPayment("PAY-17S8410768582940NKEE66EQ")
```
### Get list of payments
```go
payments, err := c.GetPayments()
```
### Get authorization by ID
## Get authorization by ID
```go
auth, err := c.GetAuthorization("2DC87612EK520411B")
@ -133,7 +34,7 @@ auth, err := c.GetAuthorization("2DC87612EK520411B")
### Capture authorization
```go
capture, err := c.CaptureAuthorization(authID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"}, true)
capture, err := c.CaptureAuthorization(authID, &paypal.Amount{Total: "7.00", Currency: "USD"}, true)
```
### Void authorization
@ -145,22 +46,7 @@ auth, err := c.VoidAuthorization(authID)
### Reauthorize authorization
```go
auth, err := c.ReauthorizeAuthorization(authID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"})
```
### Get Sale by ID
```go
sale, err := c.GetSale("36C38912MN9658832")
```
### Refund Sale by ID
```go
// Full
refund, err := c.RefundSale(saleID, nil)
// Partial
refund, err := c.RefundSale(saleID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"})
auth, err := c.ReauthorizeAuthorization(authID, &paypal.Amount{Total: "7.00", Currency: "USD"})
```
### Get Refund by ID
@ -175,22 +61,32 @@ refund, err := c.GetRefund("O-4J082351X3132253H")
order, err := c.GetOrder("O-4J082351X3132253H")
```
### Create an Order
```go
ctx := context.Background()
units := []paypal.PurchaseUnitRequest{}
source := &paypal.PaymentSource{}
appCtx := &paypal.ApplicationContext{}
order, err := c.CreateOrder(ctx, paypal.OrderIntentCapture, units, ource, appCtx)
```
### Update Order by ID
```go
order, err := c.UpdateOrder("O-4J082351X3132253H", []paypal.PurchaseUnitRequest{})
```
### Authorize Order
```go
auth, err := c.AuthorizeOrder(orderID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"})
auth, err := c.AuthorizeOrder(orderID, paypal.AuthorizeOrderRequest{})
```
### Capture Order
```go
capture, err := c.CaptureOrder(orderID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"}, true, nil)
```
### Void Order
```go
order, err := c.VoidOrder(orderID)
capture, err := c.CaptureOrder(orderID, paypal.CaptureOrderRequest{})
```
### Identity
@ -210,15 +106,15 @@ userInfo, err := c.GetUserInfo("openid")
### Create single payout to email
```go
payout := paypalsdk.Payout{
SenderBatchHeader: &paypalsdk.SenderBatchHeader{
payout := paypal.Payout{
SenderBatchHeader: &paypal.SenderBatchHeader{
EmailSubject: "Subject will be displayed on PayPal",
},
Items: []paypalsdk.PayoutItem{
paypalsdk.PayoutItem{
Items: []paypal.PayoutItem{
paypal.PayoutItem{
RecipientType: "EMAIL",
Receiver: "single-email-payout@mail.com",
Amount: &paypalsdk.AmountPayout{
Amount: &paypal.AmountPayout{
Value: "15.11",
Currency: "USD",
},
@ -228,7 +124,7 @@ payout := paypalsdk.Payout{
},
}
payoutResp, err := c.CreateSinglePayout(payout)
payoutResp, err := c.CreatePayout(payout)
```
### Get payout by ID
@ -290,7 +186,6 @@ webprofiles, err := c.GetWebProfiles()
### Update web experience profile
```go
webprofile := WebProfile{
ID: "XP-CP6S-W9DY-96H8-MVN2",
Name: "Shop YeowZa! YeowZa! ",
@ -308,7 +203,7 @@ err := c.DeleteWebProfile("XP-CP6S-W9DY-96H8-MVN2")
```go
// Store CC
c.StoreCreditCard(paypalsdk.CreditCard{
c.StoreCreditCard(paypal.CreditCard{
Number: "4417119669820331",
Type: "visa",
ExpireMonth: "11",
@ -322,8 +217,8 @@ c.StoreCreditCard(paypalsdk.CreditCard{
c.DeleteCreditCard("CARD-ID-123")
// Edit it
c.PatchCreditCard("CARD-ID-123", []paypalsdk.CreditCardField{
paypalsdk.CreditCardField{
c.PatchCreditCard("CARD-ID-123", []paypal.CreditCardField{
paypal.CreditCardField{
Operation: "replace",
Path: "/billing_address/line1",
Value: "New value",
@ -337,14 +232,80 @@ c.GetCreditCard("CARD-ID-123")
c.GetCreditCards(nil)
```
### How to Contribute
### Webhooks
* Fork a repository
* Add/Fix something
* Check that tests are passing
* Create PR
```go
// Create a webhook
c.CreateWebhook(paypal.CreateWebhookRequest{
URL: "webhook URL",
EventTypes: []paypal.WebhookEventType{
paypal.WebhookEventType{
Name: "PAYMENT.AUTHORIZATION.CREATED",
},
},
})
### Tests
// Update a registered webhook
c.UpdateWebhook("WebhookID", []paypal.WebhookField{
paypal.WebhookField{
Operation: "replace",
Path: "/event_types",
Value: []interface{}{
map[string]interface{}{
"name": "PAYMENT.SALE.REFUNDED",
},
},
},
})
* Unit tests: `go test`
* Integration tests: `go test -tags=integration`
// Get a registered webhook
c.GetWebhook("WebhookID")
// Delete a webhook
c.DeleteWebhook("WebhookID")
// List registered webhooks
c.ListWebhooks(paypal.AncorTypeApplication)
```
### Generate Next Invoice Number
```go
// GenerateInvoiceNumber: generates the next invoice number that is available to the merchant.
c.GenerateInvoiceNumber(ctx) // might return something like "0001" or "0010".
```
### Get Invoice Details by ID
```go
// the second argument is an ID, it should be valid
invoice, err := c.GetInvoiceDetails(ctx, "INV2-XFXV-YW42-ZANU-4F33")
```
- for now, we are yet to implement the ShowAllInvoices endpoint, so use the following cURL request for the same(this gives you the list of invoice-IDs for this customer)
```bash
curl -v -X GET https://api-m.sandbox.paypal.com/v2/invoicing/invoices?total_required=true \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <Token>"
```
- refer to the beginning of this Usage section for obtaining a Token.
## How to Contribute
- Fork a repository
- Add/Fix something
- Check that tests are passing
- Create PR
Main contributors:
- [Alex Pliutau](https://github.com/plutov)
- [Roopak Venkatakrishnan](https://github.com/roopakv)
## Tests
```
go test -v ./...
```

View File

@ -1,68 +1,84 @@
package paypalsdk
package paypal
import (
"bytes"
"context"
"fmt"
"net/http"
"strconv"
)
// GetAuthorization returns an authorization by ID
// Endpoint: GET /v1/payments/authorization/ID
func (c *Client) GetAuthorization(authID string) (*Authorization, error) {
// Endpoint: GET /v2/payments/authorizations/ID
func (c *Client) GetAuthorization(ctx context.Context, authID string) (*Authorization, error) {
buf := bytes.NewBuffer([]byte(""))
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v1/payments/authorization/", authID), buf)
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v2/payments/authorizations/", authID), buf)
auth := &Authorization{}
if err != nil {
return &Authorization{}, err
return auth, err
}
auth := &Authorization{}
err = c.SendWithAuth(req, auth)
return auth, err
}
// CaptureAuthorization captures and process an existing authorization.
// To use this method, the original payment must have Intent set to "authorize"
// Endpoint: POST /v1/payments/authorization/ID/capture
func (c *Client) CaptureAuthorization(authID string, a *Amount, isFinalCapture bool) (*Capture, error) {
isFinalStr := strconv.FormatBool(isFinalCapture)
// Endpoint: POST /v2/payments/authorizations/ID/capture
func (c *Client) CaptureAuthorization(ctx context.Context, authID string, paymentCaptureRequest *PaymentCaptureRequest) (*PaymentCaptureResponse, error) {
return c.CaptureAuthorizationWithPaypalRequestId(ctx, authID, paymentCaptureRequest, "")
}
// CaptureAuthorization captures and process an existing authorization with idempotency.
// To use this method, the original payment must have Intent set to "authorize"
// Endpoint: POST /v2/payments/authorizations/ID/capture
func (c *Client) CaptureAuthorizationWithPaypalRequestId(ctx context.Context,
authID string,
paymentCaptureRequest *PaymentCaptureRequest,
requestID string,
) (*PaymentCaptureResponse, error) {
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/payments/authorizations/"+authID+"/capture"), paymentCaptureRequest)
paymentCaptureResponse := &PaymentCaptureResponse{}
buf := bytes.NewBuffer([]byte("{\"amount\":{\"currency\":\"" + a.Currency + "\",\"total\":\"" + a.Total + "\"},\"is_final_capture\":" + isFinalStr + "}"))
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/authorization/"+authID+"/capture"), buf)
if err != nil {
return &Capture{}, err
return paymentCaptureResponse, err
}
capture := &Capture{}
err = c.SendWithAuth(req, capture)
return capture, err
if requestID != "" {
req.Header.Set("PayPal-Request-Id", requestID)
}
err = c.SendWithAuth(req, paymentCaptureResponse)
return paymentCaptureResponse, err
}
// VoidAuthorization voids a previously authorized payment
// Endpoint: POST /v1/payments/authorization/ID/void
func (c *Client) VoidAuthorization(authID string) (*Authorization, error) {
// Endpoint: POST /v2/payments/authorizations/ID/void
func (c *Client) VoidAuthorization(ctx context.Context, authID string) (*Authorization, error) {
buf := bytes.NewBuffer([]byte(""))
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/authorization/"+authID+"/void"), buf)
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/payments/authorizations/"+authID+"/void"), buf)
auth := &Authorization{}
if err != nil {
return &Authorization{}, err
return auth, err
}
auth := &Authorization{}
err = c.SendWithAuth(req, auth)
return auth, err
}
// ReauthorizeAuthorization reauthorize a Paypal account payment.
// PayPal recommends to reauthorize payment after ~3 days
// Endpoint: POST /v1/payments/authorization/ID/reauthorize
func (c *Client) ReauthorizeAuthorization(authID string, a *Amount) (*Authorization, error) {
buf := bytes.NewBuffer([]byte("{\"amount\":{\"currency\":\"" + a.Currency + "\",\"total\":\"" + a.Total + "\"}}"))
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/authorization/"+authID+"/reauthorize"), buf)
// PayPal recommends reauthorizing payment after ~3 days
// Endpoint: POST /v2/payments/authorizations/ID/reauthorize
func (c *Client) ReauthorizeAuthorization(ctx context.Context, authID string, a *Amount) (*Authorization, error) {
buf := bytes.NewBuffer([]byte(`{"amount":{"currency_code":"` + a.Currency + `","value":"` + a.Total + `"}}`))
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/payments/authorizations/"+authID+"/reauthorize"), buf)
auth := &Authorization{}
if err != nil {
return &Authorization{}, err
return auth, err
}
auth := &Authorization{}
err = c.SendWithAuth(req, auth)
return auth, err
}

View File

@ -1,98 +0,0 @@
package paypalsdk
import (
"bytes"
"errors"
"fmt"
"net/http"
"time"
)
type (
// CreateBillingResp struct
CreateBillingResp struct {
ID string `json:"id,omitempty"`
State string `json:"state,omitempty"`
PaymentDefinitions []PaymentDefinition `json:"payment_definitions,omitempty"`
MerchantPreferences MerchantPreferences `json:"merchant_preferences,omitempty"`
CreateTime time.Time `json:"create_time,omitempty"`
UpdateTime time.Time `json:"update_time,omitempty"`
Links []Link `json:"links,omitempty"`
}
// CreateAgreementResp struct
CreateAgreementResp struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Plan BillingPlan `json:"plan,omitempty"`
Links []Link `json:"links,omitempty"`
StartTime time.Time `json:"start_time,omitempty"`
}
)
// CreateBillingPlan creates a billing plan in Paypal
// Endpoint: POST /v1/payments/billing-plans
func (c *Client) CreateBillingPlan(plan BillingPlan) (*CreateBillingResp, error) {
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-plans"), plan)
response := &CreateBillingResp{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// ActivatePlan activates a billing plan
// By default, a new plan is not activated
// Endpoint: PATCH /v1/payments/billing-plans/
func (c *Client) ActivatePlan(planID string) error {
buf := bytes.NewBuffer([]byte("[{\"op\":\"replace\",\"path\":\"/\",\"value\":{\"state\":\"ACTIVE\"}}]"))
req, err := http.NewRequest("PATCH", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-plans/"+planID), buf)
if err != nil {
return err
}
req.SetBasicAuth(c.ClientID, c.Secret)
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
return c.SendWithAuth(req, nil)
}
// CreateBillingAgreement creates an agreement for specified plan
// Endpoint: POST /v1/payments/billing-agreements
func (c *Client) CreateBillingAgreement(a BillingAgreement) (*CreateAgreementResp, error) {
// PayPal needs only ID, so we will remove all fields except Plan ID
a.Plan = BillingPlan{
ID: a.Plan.ID,
}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-agreements"), a)
response := &CreateAgreementResp{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// ExecuteApprovedAgreement - Use this call to execute (complete) a PayPal agreement that has been approved by the payer.
// Endpoint: POST /v1/payments/billing-agreements/token/agreement-execute
func (c *Client) ExecuteApprovedAgreement(token string) (*ExecuteAgreementResponse, error) {
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/billing-agreements/"+token+"/agreement-execute"), nil)
if err != nil {
return &ExecuteAgreementResponse{}, err
}
req.SetBasicAuth(c.ClientID, c.Secret)
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
e := ExecuteAgreementResponse{}
if err = c.SendWithAuth(req, &e); err != nil {
return &e, err
}
if e.ID == "" {
return &e, errors.New("Unable to execute agreement with token=" + token)
}
return &e, err
}

View File

@ -1,108 +0,0 @@
package paypalsdk_test
import (
"fmt"
"time"
pp "github.com/logpacker/PayPal-Go-SDK"
)
func BillingExample() {
plan := pp.BillingPlan{
Name: "Plan with Regular and Trial Payment Definitions",
Description: "Plan with regular and trial payment definitions.",
Type: "fixed",
PaymentDefinitions: []pp.PaymentDefinition{
pp.PaymentDefinition{
Name: "Regular payment definition",
Type: "REGULAR",
Frequency: "MONTH",
FrequencyInterval: "2",
Amount: pp.AmountPayout{
Value: "100",
Currency: "USD",
},
Cycles: "12",
ChargeModels: []pp.ChargeModel{
pp.ChargeModel{
Type: "SHIPPING",
Amount: pp.AmountPayout{
Value: "10",
Currency: "USD",
},
},
pp.ChargeModel{
Type: "TAX",
Amount: pp.AmountPayout{
Value: "12",
Currency: "USD",
},
},
},
},
pp.PaymentDefinition{
Name: "Trial payment definition",
Type: "trial",
Frequency: "week",
FrequencyInterval: "5",
Amount: pp.AmountPayout{
Value: "9.19",
Currency: "USD",
},
Cycles: "2",
ChargeModels: []pp.ChargeModel{
pp.ChargeModel{
Type: "SHIPPING",
Amount: pp.AmountPayout{
Value: "1",
Currency: "USD",
},
},
pp.ChargeModel{
Type: "TAX",
Amount: pp.AmountPayout{
Value: "2",
Currency: "USD",
},
},
},
},
},
MerchantPreferences: &pp.MerchantPreferences{
SetupFee: &pp.AmountPayout{
Value: "1",
Currency: "USD",
},
ReturnURL: "http://www.paypal.com",
CancelURL: "http://www.paypal.com/cancel",
AutoBillAmount: "YES",
InitialFailAmountAction: "CONTINUE",
MaxFailAttempts: "0",
},
}
c, err := pp.NewClient("clientID", "secretID", pp.APIBaseSandBox)
if err != nil {
panic(err)
}
_, err = c.GetAccessToken()
if err != nil {
panic(err)
}
planResp, err := c.CreateBillingPlan(plan)
if err != nil {
panic(err)
}
err = c.ActivatePlan(planResp.ID)
fmt.Println(err)
agreement := pp.BillingAgreement{
Name: "Fast Speed Agreement",
Description: "Agreement for Fast Speed Plan",
StartDate: pp.JSONTime(time.Now().Add(time.Hour * 24)),
Plan: pp.BillingPlan{ID: planResp.ID},
Payer: pp.Payer{
PaymentMethod: "paypal",
},
}
resp, err := c.CreateBillingAgreement(agreement)
fmt.Println(err, resp)
}

113
client.go
View File

@ -1,19 +1,19 @@
package paypalsdk
package paypal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"time"
)
// NewClient returns new Client struct
// APIBase is a base API URL, for testing you can use paypalsdk.APIBaseSandBox
// APIBase is a base API URL, for testing you can use paypal.APIBaseSandBox
func NewClient(clientID string, secret string, APIBase string) (*Client, error) {
if clientID == "" || secret == "" || APIBase == "" {
return nil, errors.New("ClientID, Secret and APIBase are required to create a Client")
@ -30,25 +30,25 @@ func NewClient(clientID string, secret string, APIBase string) (*Client, error)
// GetAccessToken returns struct of TokenResponse
// No need to call SetAccessToken to apply new access token for current Client
// Endpoint: POST /v1/oauth2/token
func (c *Client) GetAccessToken() (*TokenResponse, error) {
func (c *Client) GetAccessToken(ctx context.Context) (*TokenResponse, error) {
buf := bytes.NewBuffer([]byte("grant_type=client_credentials"))
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/oauth2/token"), buf)
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/oauth2/token"), buf)
if err != nil {
return &TokenResponse{}, err
}
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
t := TokenResponse{}
err = c.SendWithBasicAuth(req, &t)
response := &TokenResponse{}
err = c.SendWithBasicAuth(req, response)
// Set Token fur current Client
if t.Token != "" {
c.Token = &t
c.tokenExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn) * time.Second)
if response.Token != "" {
c.Token = response
c.tokenExpiresAt = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second)
}
return &t, err
return response, err
}
// SetHTTPClient sets *http.Client to current client
@ -65,13 +65,19 @@ func (c *Client) SetAccessToken(token string) {
}
// SetLog will set/change the output destination.
// If log file is set paypalsdk will log all requests and responses to this Writer
// If log file is set paypal will log all requests and responses to this Writer
func (c *Client) SetLog(log io.Writer) {
c.Log = log
}
// SetReturnRepresentation enables verbose response
// Verbose response: https://developer.paypal.com/docs/api/orders/v2/#orders-authorize-header-parameters
func (c *Client) SetReturnRepresentation() {
c.returnRepresentation = true
}
// Send makes a request to the API, the response body will be
// unmarshaled into v, or if v is an io.Writer, the response will
// unmarshalled into v, or if v is an io.Writer, the response will
// be written to it without decoding
func (c *Client) Send(req *http.Request, v interface{}) error {
var (
@ -88,33 +94,50 @@ func (c *Client) Send(req *http.Request, v interface{}) error {
if req.Header.Get("Content-type") == "" {
req.Header.Set("Content-type", "application/json")
}
if c.returnRepresentation {
req.Header.Set("Prefer", "return=representation")
}
if c.Log != nil {
if reqDump, err := httputil.DumpRequestOut(req, true); err == nil {
c.Log.Write([]byte(fmt.Sprintf("Request: %s\n", reqDump)))
}
}
resp, err = c.Client.Do(req)
c.log(req, resp)
if err != nil {
return err
}
defer resp.Body.Close()
if c.Log != nil {
if respDump, err := httputil.DumpResponse(resp, true); err == nil {
c.Log.Write([]byte(fmt.Sprintf("Response from %s: %s\n", req.URL, respDump)))
}
}
defer func(Body io.ReadCloser) error {
return Body.Close()
}(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode > 299 {
errResp := &ErrorResponse{Response: resp}
data, err = ioutil.ReadAll(resp.Body)
data, err = io.ReadAll(resp.Body)
if err == nil && len(data) > 0 {
json.Unmarshal(data, errResp)
err := json.Unmarshal(data, errResp)
if err != nil {
return err
}
}
return errResp
}
if v == nil {
return nil
}
if w, ok := v.(io.Writer); ok {
io.Copy(w, resp.Body)
return nil
_, err := io.Copy(w, resp.Body)
return err
}
return json.NewDecoder(resp.Body).Decode(v)
@ -125,17 +148,25 @@ func (c *Client) Send(req *http.Request, v interface{}) error {
// making the main request
// client.Token will be updated when changed
func (c *Client) SendWithAuth(req *http.Request, v interface{}) error {
if c.Token != nil {
if !c.tokenExpiresAt.IsZero() && c.tokenExpiresAt.Sub(time.Now()) < RequestNewTokenBeforeExpiresIn {
// c.Token will be updated in GetAccessToken call
if _, err := c.GetAccessToken(); err != nil {
return err
}
}
// c.Lock()
c.mu.Lock()
// Note: Here we do not want to `defer c.Unlock()` because we need `c.Send(...)`
// to happen outside of the locked section.
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
if c.Token == nil || (!c.tokenExpiresAt.IsZero() && time.Until(c.tokenExpiresAt) < RequestNewTokenBeforeExpiresIn) {
// c.Token will be updated in GetAccessToken call
if _, err := c.GetAccessToken(req.Context()); err != nil {
// c.Unlock()
c.mu.Unlock()
return err
}
}
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
// Unlock the client mutex before sending the request, this allows multiple requests
// to be in progress at the same time.
// c.Unlock()
c.mu.Unlock()
return c.Send(req, v)
}
@ -148,34 +179,14 @@ func (c *Client) SendWithBasicAuth(req *http.Request, v interface{}) error {
// NewRequest constructs a request
// Convert payload to a JSON
func (c *Client) NewRequest(method, url string, payload interface{}) (*http.Request, error) {
func (c *Client) NewRequest(ctx context.Context, method, url string, payload interface{}) (*http.Request, error) {
var buf io.Reader
if payload != nil {
var b []byte
b, err := json.Marshal(&payload)
if err != nil {
return nil, err
}
buf = bytes.NewBuffer(b)
}
return http.NewRequest(method, url, buf)
}
// log will dump request and response to the log file
func (c *Client) log(r *http.Request, resp *http.Response) {
if c.Log != nil {
var (
reqDump string
respDump []byte
)
if r != nil {
reqDump = fmt.Sprintf("%s %s. Data: %s", r.Method, r.URL.String(), r.Form.Encode())
}
if resp != nil {
respDump, _ = httputil.DumpResponse(resp, true)
}
c.Log.Write([]byte(fmt.Sprintf("Request: %s\nResponse: %s\n", reqDump, string(respDump))))
}
return http.NewRequestWithContext(ctx, method, url, buf)
}

94
client_test.go Normal file
View File

@ -0,0 +1,94 @@
package paypal
import (
"context"
"fmt"
"math/rand"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
const alphabet = "abcedfghijklmnopqrstuvwxyz"
var testClientID = "AXy9orp-CDaHhBZ9C78QHW2BKZpACgroqo85_NIOa9mIfJ9QnSVKzY-X_rivR_fTUUr6aLjcJsj6sDur"
var testSecret = "EBoIiUSkCKeSk49hHSgTem1qnjzzJgRQHDEHvGpzlLEf_nIoJd91xu8rPOBDCdR_UYNKVxJE-UgS2iCw"
func RandomString(n int) string {
var sb strings.Builder
k := len(alphabet)
for i := 0; i < n; i++ {
c := alphabet[rand.Intn(k)]
sb.WriteByte(c)
}
return sb.String()
}
func createRandomProduct(t *testing.T) Product {
//create a product
productData := Product{
Name: RandomString(10),
Description: RandomString(100),
Category: ProductCategorySoftware,
Type: ProductTypeService,
ImageUrl: "https://example.com/image.png",
HomeUrl: "https://example.com",
}
return productData
}
// this is a simple copy of the SendWithAuth method, used to
// test the Lock and Unlock methods of the private mutex field
// of Client structure.
func (c *Client) sendWithAuth(req *http.Request, v interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.Token == nil || (!c.tokenExpiresAt.IsZero() && time.Until(c.tokenExpiresAt) < RequestNewTokenBeforeExpiresIn) {
if _, err := c.GetAccessToken(req.Context()); err != nil {
return err
}
}
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
return c.Send(req, v)
}
// this method is used to invoke the sendWithAuth method, which will then check
// operationally the privated mutex field of Client structure.
func (c *Client) createProduct(ctx context.Context, product Product) (*CreateProductResponse, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/catalogs/products"), product)
response := &CreateProductResponse{}
if err != nil {
return response, err
}
err = c.sendWithAuth(req, response)
return response, err
}
func TestClientMutex(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken(context.Background())
// Operational testing of the private mutex field
n_iter := 2
errs := make(chan error)
for i := 0; i < n_iter; i++ {
go func() {
_, err := c.createProduct(context.Background(), createRandomProduct(t))
errs <- err
}()
}
for i := 0; i < n_iter; i++ {
err := <-errs
assert.Equal(t, nil, err)
}
}

118
const.go Normal file
View File

@ -0,0 +1,118 @@
package paypal
type SubscriptionPlanStatus string
const (
SubscriptionPlanStatusCreated SubscriptionPlanStatus = "CREATED"
SubscriptionPlanStatusInactive SubscriptionPlanStatus = "INACTIVE"
SubscriptionPlanStatusActive SubscriptionPlanStatus = "ACTIVE"
)
type BillingPlanStatus string
const (
BillingPlanStatusActive BillingPlanStatus = "ACTIVE"
)
type IntervalUnit string
const (
IntervalUnitDay IntervalUnit = "DAY"
IntervalUnitWeek IntervalUnit = "WEEK"
IntervalUnitMonth IntervalUnit = "MONTH"
IntervalUnitYear IntervalUnit = "YEAR"
)
type TenureType string
const (
TenureTypeRegular TenureType = "REGULAR"
TenureTypeTrial TenureType = "TRIAL"
)
type SetupFeeFailureAction string
const (
SetupFeeFailureActionContinue SetupFeeFailureAction = "CONTINUE"
SetupFeeFailureActionCancel SetupFeeFailureAction = "CANCEL"
)
type ShippingPreference string
const (
ShippingPreferenceGetFromFile ShippingPreference = "GET_FROM_FILE"
ShippingPreferenceNoShipping ShippingPreference = "NO_SHIPPING"
ShippingPreferenceSetProvidedAddress ShippingPreference = "SET_PROVIDED_ADDRESS"
)
type UserAction string
const (
UserActionContinue UserAction = "CONTINUE"
UserActionPayNow UserAction = "PAY_NOW"
UserActionSubscribeNow UserAction = "SUBSCRIBE_NOW"
)
type SubscriptionStatus string
const (
SubscriptionStatusApprovalPending SubscriptionStatus = "APPROVAL_PENDING"
SubscriptionStatusApproved SubscriptionStatus = "APPROVED"
SubscriptionStatusActive SubscriptionStatus = "ACTIVE"
SubscriptionStatusSuspended SubscriptionStatus = "SUSPENDED"
SubscriptionStatusCancelled SubscriptionStatus = "CANCELLED"
SubscriptionStatusExpired SubscriptionStatus = "EXPIRED"
)
//Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-transaction
type SubscriptionTransactionStatus string
const (
SubscriptionCaptureStatusCompleted SubscriptionTransactionStatus = "COMPLETED"
SubscriptionCaptureStatusDeclined SubscriptionTransactionStatus = "DECLINED"
SubscriptionCaptureStatusPartiallyRefunded SubscriptionTransactionStatus = "PARTIALLY_REFUNDED"
SubscriptionCaptureStatusPending SubscriptionTransactionStatus = "PENDING"
SubscriptionCaptureStatusRefunded SubscriptionTransactionStatus = "REFUNDED"
)
type CaptureType string
const (
CaptureTypeOutstandingBalance CaptureType = "OUTSTANDING_BALANCE"
)
type ProductType string
type ProductCategory string //Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#definition-product_category
const (
ProductTypePhysical ProductType = "PHYSICAL"
ProductTypeDigital ProductType = "DIGITAL"
ProductTypeService ProductType = "SERVICE"
ProductCategorySoftware ProductCategory = "SOFTWARE"
ProductCategorySoftwareComputerAndDataProcessingServices ProductCategory = "COMPUTER_AND_DATA_PROCESSING_SERVICES"
ProductCategorySoftwareDigitalGames ProductCategory = "DIGITAL_GAMES"
ProductCategorySoftwareGameSoftware ProductCategory = "GAME_SOFTWARE"
ProductCategorySoftwareGames ProductCategory = "GAMES"
ProductCategorySoftwareGeneral ProductCategory = "GENERAL"
ProductCategorySoftwareGraphicAndCommercialDesign ProductCategory = "GRAPHIC_AND_COMMERCIAL_DESIGN"
ProductCategorySoftwareOemSoftware ProductCategory = "OEM_SOFTWARE"
ProductCategorySoftwareOnlineGaming ProductCategory = "ONLINE_GAMING"
ProductCategorySoftwareOnlineGamingCurrency ProductCategory = "ONLINE_GAMING_CURRENCY"
ProductCategorySoftwareOnlineServices ProductCategory = "ONLINE_SERVICES"
ProductCategorySoftwareOther ProductCategory = "OTHER"
ProductCategorySoftwareServices ProductCategory = "SERVICES"
)
type PayeePreferred string // Doc: https://developer.paypal.com/api/orders/v2/#definition-payment_method
const (
PayeePreferredUnrestricted PayeePreferred = "UNRESTRICTED"
PayeePreferredImmediatePaymentRequired PayeePreferred = "IMMEDIATE_PAYMENT_REQUIRED"
)
type StandardEntryClassCode string // Doc: https://developer.paypal.com/api/orders/v2/#definition-payment_method
const (
StandardEntryClassCodeTel StandardEntryClassCode="TEL"
StandardEntryClassCodeWeb StandardEntryClassCode="WEB"
StandardEntryClassCodeCcd StandardEntryClassCode="CCD"
StandardEntryClassCodePpd StandardEntryClassCode="PPD"
)

10
doc.go
View File

@ -1,10 +1,10 @@
/*
Package paypalsdk provides a wrapper to PayPal API (https://developer.paypal.com/webapps/developer/docs/api/).
The first thing you do is to create a Client (you can select API base URL using paypalsdk contants).
c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox)
Package paypal provides a wrapper to PayPal API (https://developer.paypal.com/webapps/developer/docs/api/).
The first thing you do is to create a Client (you can select API base URL using paypal contants).
c, err := paypal.NewClient("clientID", "secretID", paypal.APIBaseSandBox)
Then you can get an access token from PayPal:
accessToken, err := c.GetAccessToken()
After you have an access token you can call built-in functions to get data from PayPal.
paypalsdk will assign all responses to go structures.
paypal will assign all responses to go structures.
*/
package paypalsdk
package paypal

View File

@ -1,51 +0,0 @@
package paypalsdk_test
import paypalsdk "github.com/logpacker/PayPal-Go-SDK"
func Example() {
// Initialize client
c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox)
if err != nil {
panic(err)
}
// Retrieve access token
_, err = c.GetAccessToken()
if err != nil {
panic(err)
}
// Create credit card payment
p := paypalsdk.Payment{
Intent: "sale",
Payer: &paypalsdk.Payer{
PaymentMethod: "credit_card",
FundingInstruments: []paypalsdk.FundingInstrument{{
CreditCard: &paypalsdk.CreditCard{
Number: "4111111111111111",
Type: "visa",
ExpireMonth: "11",
ExpireYear: "2020",
CVV2: "777",
FirstName: "John",
LastName: "Doe",
},
}},
},
Transactions: []paypalsdk.Transaction{{
Amount: &paypalsdk.Amount{
Currency: "USD",
Total: "7.00",
},
Description: "My Payment",
}},
RedirectURLs: &paypalsdk.RedirectURLs{
ReturnURL: "http://...",
CancelURL: "http://...",
},
}
_, err = c.CreatePayment(p)
if err != nil {
panic(err)
}
}

61
filter.go Normal file
View File

@ -0,0 +1,61 @@
package paypal
import (
"fmt"
"time"
)
const format = "2006-01-02T15:04:05Z"
// Filter type
type Filter struct {
fields []fmt.Stringer
}
func (s *Filter) String() string {
var filter string
for i, f := range s.fields {
if i == 0 {
filter = "?" + f.String()
} else {
filter = filter + "&" + f.String()
}
}
return filter
}
// TextField type
type TextField struct {
name string
Is string
}
func (d TextField) String() string {
return fmt.Sprintf("%s=%s", d.name, d.Is)
}
// TimeField type
type TimeField struct {
name string
Is time.Time
}
// String .
func (d TimeField) String() string {
return fmt.Sprintf("%s=%s", d.name, d.Is.UTC().Format(format))
}
// AddTextField .
func (s *Filter) AddTextField(field string) *TextField {
f := &TextField{name: field}
s.fields = append(s.fields, f)
return f
}
// AddTimeField .
func (s *Filter) AddTimeField(field string) *TimeField {
f := &TimeField{name: field}
s.fields = append(s.fields, f)
return f
}

44
filter_test.go Normal file
View File

@ -0,0 +1,44 @@
package paypal
import (
"testing"
"time"
)
func TestFilter_AddTextField(t *testing.T) {
filter := &Filter{}
filter.AddTextField("sort_by").Is = "create_time"
filter.AddTextField("count").Is = "30"
filter.AddTextField("sort_order").Is = "desc"
expected := "?sort_by=create_time&count=30&sort_order=desc"
if filter.String() != expected {
t.Errorf("filter string was %s, wanted %s", filter.String(), expected)
}
}
func TestFilter_AddTimeField(t *testing.T) {
filter := &Filter{}
startTime := time.Time{}
endTime := startTime.Add(time.Hour * 24 * 30)
filter.AddTimeField("start_time").Is = startTime
filter.AddTimeField("stop_time").Is = endTime
expected := "?start_time=0001-01-01T00:00:00Z&stop_time=0001-01-31T00:00:00Z"
if filter.String() != expected {
t.Errorf("filter string was %s, wanted %s", filter.String(), expected)
}
}
func TestFilter_AddMixedFields(t *testing.T) {
filter := &Filter{}
startTime := time.Time{}
endTime := startTime.Add(time.Hour * 24 * 30)
filter.AddTimeField("stop_time").Is = endTime
filter.AddTextField("count").Is = "30"
expected := "?stop_time=0001-01-31T00:00:00Z&count=30"
if filter.String() != expected {
t.Errorf("filter string was %s, wanted %s", filter.String(), expected)
}
}

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module euphoria-laxis.fr/go-packages/paypale/v4
go 1.23
require github.com/stretchr/testify v1.9.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,72 +1,24 @@
package paypalsdk
package paypal
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
)
// GrantNewAccessTokenFromAuthCode - Use this call to grant a new access token, using the previously obtained authorization code.
// Endpoint: POST /v1/identity/openidconnect/tokenservice
func (c *Client) GrantNewAccessTokenFromAuthCode(code string, redirectURI string) (*TokenResponse, error) {
token := &TokenResponse{}
q := url.Values{}
q.Set("grant_type", "authorization_code")
q.Set("code", code)
q.Set("redirect_uri", redirectURI)
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/identity/openidconnect/tokenservice"), strings.NewReader(q.Encode()))
if err != nil {
return token, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if err = c.SendWithBasicAuth(req, token); err != nil {
return token, err
}
return token, nil
}
// GrantNewAccessTokenFromRefreshToken - Use this call to grant a new access token, using a refresh token.
// Endpoint: POST /v1/identity/openidconnect/tokenservice
func (c *Client) GrantNewAccessTokenFromRefreshToken(refreshToken string) (*TokenResponse, error) {
type request struct {
GrantType string `json:"grant_type"`
RefreshToken string `json:"refresh_token"`
}
token := &TokenResponse{}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/identity/openidconnect/tokenservice"), request{GrantType: "refresh_token", RefreshToken: refreshToken})
if err != nil {
return token, err
}
if err = c.SendWithAuth(req, token); err != nil {
return token, err
}
return token, nil
}
// GetUserInfo - Use this call to retrieve user profile attributes.
// Endpoint: GET /v1/identity/openidconnect/userinfo/?schema=<Schema>
// Pass the schema that is used to return as per openidconnect protocol. The only supported schema value is openid.
func (c *Client) GetUserInfo(schema string) (*UserInfo, error) {
u := UserInfo{}
func (c *Client) GetUserInfo(ctx context.Context, schema string) (*UserInfo, error) {
u := &UserInfo{}
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v1/identity/openidconnect/userinfo/?schema=", schema), nil)
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v1/identity/openidconnect/userinfo/?schema=", schema), nil)
if err != nil {
return &u, err
return u, err
}
if err = c.SendWithAuth(req, &u); err != nil {
return &u, err
if err = c.SendWithAuth(req, u); err != nil {
return u, err
}
return &u, nil
return u, nil
}

View File

@ -1,437 +0,0 @@
// +build integration
package paypalsdk
import (
"testing"
)
// All test values are defined here
var testClientID = "AZgwu4yt5Ba0gyTu1dGBH3txHCJbMuFNvrmQxBaQbfDncDiCs6W_rwJD8Ir-0pZrN-_eq7n9zVd8Y-5f"
var testSecret = "EBzA1wRl5t73OMugOieDj_tI3vihfJmGl47ukQT-cpctooIzDu0K7IPESNC0cKodlLSOXzwI8qXSM0rd"
var testAuthID = "2DC87612EK520411B"
var testOrderID = "O-0PW72302W3743444R"
var testSaleID = "4CF18861HF410323U"
var testPaymentID = "PAY-5YK922393D847794YKER7MUI"
var testPayerID = "CR87QHB7JTRSC"
var testUserID = "https://www.paypal.com/webapps/auth/identity/user/WEssgRpQij92sE99_F9MImvQ8FPYgUEjrvCja2qH2H8"
var testCardID = "CARD-54E6956910402550WKGRL6EA"
func TestGetAccessToken(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
token, err := c.GetAccessToken()
if err != nil {
t.Errorf("Not expected error for GetAccessToken(), got %s", err.Error())
}
if token.Token == "" {
t.Errorf("Expected non-empty token for GetAccessToken()")
}
}
func TestGetAuthorization(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.GetAuthorization(testAuthID)
if err == nil {
t.Errorf("GetAuthorization expects error")
}
}
func TestCaptureAuthorization(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.CaptureAuthorization(testAuthID, &Amount{Total: "200", Currency: "USD"}, true)
if err == nil {
t.Errorf("Auth is expired, 400 error must be returned")
}
}
func TestVoidAuthorization(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.VoidAuthorization(testAuthID)
if err == nil {
t.Errorf("Auth is expired, 400 error must be returned")
}
}
func TestReauthorizeAuthorization(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.ReauthorizeAuthorization(testAuthID, &Amount{Total: "200", Currency: "USD"})
if err == nil {
t.Errorf("Reauthorization not allowed for this product, 500 error must be returned")
}
}
func TestGrantNewAccessTokenFromAuthCode(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
_, err := c.GrantNewAccessTokenFromAuthCode("123", "http://example.com/myapp/return.php")
if err == nil {
t.Errorf("GrantNewAccessTokenFromAuthCode must return error for invalid code")
}
}
func TestGrantNewAccessTokenFromRefreshToken(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
_, err := c.GrantNewAccessTokenFromRefreshToken("123")
if err == nil {
t.Errorf("GrantNewAccessTokenFromRefreshToken must return error for invalid refresh token")
}
}
func TestGetUserInfo(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
u, err := c.GetUserInfo("openid")
if u.ID != testUserID || err != nil {
t.Errorf("GetUserInfo must return valid test ID=%s, error: %v", testUserID, err)
}
}
func TestGetOrder(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.GetOrder(testOrderID)
if err == nil {
t.Errorf("GetOrder expects error")
}
}
func TestAuthorizeOrder(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.AuthorizeOrder(testOrderID, &Amount{Total: "7.00", Currency: "USD"})
if err == nil {
t.Errorf("Order is expired, 400 error must be returned")
}
}
func TestCaptureOrder(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.CaptureOrder(testOrderID, &Amount{Total: "100", Currency: "USD"}, true, nil)
if err == nil {
t.Errorf("Order is expired, 400 error must be returned")
}
}
func TestVoidOrder(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.VoidOrder(testOrderID)
if err == nil {
t.Errorf("Order is expired, 400 error must be returned")
}
}
func TestCreateDirectPaypalPayment(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
amount := Amount{
Total: "15.11",
Currency: "USD",
}
p, err := c.CreateDirectPaypalPayment(amount, "http://example.com", "http://example.com", "test payment")
if err != nil || p.ID == "" {
t.Errorf("Test paypal payment is not created, err: %v", err)
}
}
func TestCreatePayment(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
p := Payment{
Intent: "sale",
Payer: &Payer{
PaymentMethod: "credit_card",
FundingInstruments: []FundingInstrument{{
CreditCard: &CreditCard{
Number: "4111111111111111",
Type: "visa",
ExpireMonth: "11",
ExpireYear: "2020",
CVV2: "777",
FirstName: "John",
LastName: "Doe",
},
}},
},
Transactions: []Transaction{{
Amount: &Amount{
Currency: "USD",
Total: "10.00", // total cost including shipping
Details: Details{
Shipping: "3.00", // total shipping cost
Subtotal: "7.00", // total cost without shipping
},
},
Description: "My Payment",
ItemList: &ItemList{
Items: []Item{
Item{
Quantity: "2",
Price: "3.50",
Currency: "USD",
Name: "Product 1",
},
},
},
}},
RedirectURLs: &RedirectURLs{
ReturnURL: "http://..",
CancelURL: "http://..",
},
}
_, err := c.CreatePayment(p)
if err == nil {
t.Errorf("Expected error")
}
}
func TestGetPayment(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.GetPayment(testPaymentID)
if err == nil {
t.Errorf("404 for this payment ID")
}
}
func TestGetPayments(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.GetPayments()
if err != nil {
t.Errorf("Nil error expected, got: %s", err.Error())
}
}
func TestPatchPayment(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
p := Payment{
Intent: "sale",
Payer: &Payer{
PaymentMethod: "paypal",
},
Transactions: []Transaction{{
Amount: &Amount{
Currency: "USD",
Total: "10.00", // total cost including shipping
Details: Details{
Shipping: "3.00", // total shipping cost
Subtotal: "7.00", // total cost without shipping
},
},
Description: "My Payment",
ItemList: &ItemList{
Items: []Item{
Item{
Quantity: "2",
Price: "3.50",
Currency: "USD",
Name: "Product 1",
},
},
},
Custom: "First value",
}},
RedirectURLs: &RedirectURLs{
ReturnURL: "http://..",
CancelURL: "http://..",
},
}
NewPayment, err := c.CreatePayment(p)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
PaymentID := NewPayment.ID
pp := []PaymentPatch{
{
Operation: "replace",
Path: "/transactions/0/custom",
Value: "Replaced Value",
},
}
RevisedPayment, errpp := c.PatchPayment(PaymentID, pp)
if errpp != nil {
t.Errorf("Unexpected error when patching %v", errpp)
}
if RevisedPayment.Transactions != nil &&
len(RevisedPayment.Transactions) > 0 &&
RevisedPayment.Transactions[0].Custom != "" {
if RevisedPayment.Transactions[0].Custom != "Replaced Value" {
t.Errorf("Patched payment value failed to be patched %v", RevisedPayment.Transactions[0].Custom)
}
}
}
func TestExecuteApprovedPayment(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.ExecuteApprovedPayment(testPaymentID, testPayerID)
if err == nil {
t.Errorf("404 for this payment ID")
}
}
func TestCreateSinglePayout(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
payout := Payout{
SenderBatchHeader: &SenderBatchHeader{
EmailSubject: "Subject will be displayed on PayPal",
},
Items: []PayoutItem{
{
RecipientType: "EMAIL",
Receiver: "single-email-payout@mail.com",
Amount: &AmountPayout{
Value: "15.11",
Currency: "USD",
},
Note: "Optional note",
SenderItemID: "Optional Item ID",
},
},
}
_, err := c.CreateSinglePayout(payout)
if err != nil {
t.Errorf("Test single payout is not created, error: %v", err)
}
}
func TestGetSale(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.GetSale(testSaleID)
if err == nil {
t.Errorf("404 must be returned for ID=%s", testSaleID)
}
}
func TestRefundSale(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.RefundSale(testSaleID, nil)
if err == nil {
t.Errorf("404 must be returned for ID=%s", testSaleID)
}
_, err = c.RefundSale(testSaleID, &Amount{Total: "7.00", Currency: "USD"})
if err == nil {
t.Errorf("404 must be returned for ID=%s", testSaleID)
}
}
func TestGetRefund(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
_, err := c.GetRefund("1")
if err == nil {
t.Errorf("404 must be returned for ID=%s", testSaleID)
}
}
func TestStoreCreditCard(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
r1, e1 := c.StoreCreditCard(CreditCard{})
if e1 == nil || r1 != nil {
t.Errorf("Error is expected for invalid CC")
}
r2, e2 := c.StoreCreditCard(CreditCard{
Number: "4417119669820331",
Type: "visa",
ExpireMonth: "11",
ExpireYear: "2020",
CVV2: "874",
FirstName: "Foo",
LastName: "Bar",
})
if e2 != nil || r2 == nil {
t.Errorf("200 code expected for valid CC card. Error: %v", e2)
}
}
func TestDeleteCreditCard(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
e1 := c.DeleteCreditCard("")
if e1 == nil {
t.Errorf("Error is expected for invalid CC ID")
}
}
func TestGetCreditCard(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
r1, e1 := c.GetCreditCard("BBGGG")
if e1 == nil || r1 != nil {
t.Errorf("Error is expected for invalid CC, got CC %v", r1)
}
}
func TestGetCreditCards(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
r1, e1 := c.GetCreditCards(nil)
if e1 != nil || r1 == nil {
t.Errorf("200 code expected. Error: %v", e1)
}
r2, e2 := c.GetCreditCards(&CreditCardsFilter{
Page: 2,
PageSize: 7,
})
if e2 != nil || r2 == nil {
t.Errorf("200 code expected. Error: %v", e2)
}
}
func TestPatchCreditCard(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
r1, e1 := c.PatchCreditCard(testCardID, nil)
if e1 == nil || r1 != nil {
t.Errorf("Error is expected for empty update info")
}
}

38
invoicing.go Normal file
View File

@ -0,0 +1,38 @@
package paypal
import (
"context"
"fmt"
)
// GenerateInvoiceNumber: generates the next invoice number that is available to the merchant.
// Endpoint: POST /v2/invoicing/generate-next-invoice-number
func (c *Client) GenerateInvoiceNumber(ctx context.Context) (*InvoiceNumber, error) {
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/invoicing/generate-next-invoice-number"), nil)
nextInvoiceNumber := &InvoiceNumber{}
if err != nil {
return nextInvoiceNumber, err
}
if err = c.SendWithAuth(req, nextInvoiceNumber); err != nil {
return nextInvoiceNumber, err
}
return nextInvoiceNumber, nil
}
// GetInvoiceDetails: show invoice details for a particular invoice by ID.
// Endpoint: GET /v2/invoicing/invoices/{invoice_id}
func (c *Client) GetInvoiceDetails(ctx context.Context, invoiceID string) (*Invoice, error) {
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v2/invoicing/invoices/", invoiceID), nil)
invoice := &Invoice{}
if err != nil {
return invoice, err
}
if err = c.SendWithAuth(req, invoice); err != nil {
return invoice, err
}
return invoice, nil
}

406
invoicing_test.go Normal file
View File

@ -0,0 +1,406 @@
package paypal_test
import (
"context"
"encoding/json"
"testing"
"euphoria-laxis.fr/go-packages/paypale/v4"
"github.com/stretchr/testify/assert"
)
// All test values are defined here
var devTestClientID = "AXy9orp-CDaHhBZ9C78QHW2BKZpACgroqo85_NIOa9mIfJ9QnSVKzY-X_rivR_fTUUr6aLjcJsj6sDur"
var devTestSecret = "EBoIiUSkCKeSk49hHSgTem1qnjzzJgRQHDEHvGpzlLEf_nIoJd91xu8rPOBDCdR_UYNKVxJE-UgS2iCw"
var devAPIBaseSandBox = "https://api.sandbox.paypal.com"
func TestGenerateInvoiceNumber(t *testing.T) {
ctx := context.Background()
c, _ := paypal.NewClient(devTestClientID, devTestSecret, devAPIBaseSandBox)
_, err := c.GetAccessToken(ctx)
assert.Equal(t, nil, err)
_, err = c.GenerateInvoiceNumber(ctx)
assert.Equal(t, nil, err)
}
func assertTwoInvoices(t *testing.T, invoice paypal.Invoice, testInvoice paypal.Invoice) {
// additional_recipients
assert.Equal(t, len(invoice.AdditionalRecipients), len(testInvoice.AdditionalRecipients))
// additional_recipients --> email_address !! EQUALITY OF SPLICE OF STRUCT REMAINING !!
// amount
assert.Equal(t, invoice.AmountSummary.Currency, testInvoice.AmountSummary.Currency)
assert.Equal(t, invoice.AmountSummary.Value, testInvoice.AmountSummary.Value)
// amount-->breakdown-->custom-->amount
assert.Equal(t, invoice.AmountSummary.Breakdown.Custom.Amount.Currency, testInvoice.AmountSummary.Breakdown.Custom.Amount.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.Custom.Amount.Value, testInvoice.AmountSummary.Breakdown.Custom.Amount.Value)
// amount-->breakdown-->custom-->label
assert.Equal(t, invoice.AmountSummary.Breakdown.Custom.Label, testInvoice.AmountSummary.Breakdown.Custom.Label)
// amount-->breakdown-->discount-->amount
assert.Equal(t, invoice.AmountSummary.Breakdown.Discount.InvoiceDiscount.DiscountAmount.Currency, testInvoice.AmountSummary.Breakdown.Discount.InvoiceDiscount.DiscountAmount.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.Discount.InvoiceDiscount.DiscountAmount.Value, testInvoice.AmountSummary.Breakdown.Discount.InvoiceDiscount.DiscountAmount.Value)
// amount-->breakdown-->discount-->percent
assert.Equal(t, invoice.AmountSummary.Breakdown.Discount.InvoiceDiscount.Percent, testInvoice.AmountSummary.Breakdown.Discount.InvoiceDiscount.Percent)
// amount-->breakdown-->discount-->item_discount
assert.Equal(t, invoice.AmountSummary.Breakdown.Discount.ItemDiscount.Currency, testInvoice.AmountSummary.Breakdown.Discount.ItemDiscount.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.Discount.ItemDiscount.Value, testInvoice.AmountSummary.Breakdown.Discount.ItemDiscount.Value)
// amount-->breakdown-->item_total
assert.Equal(t, invoice.AmountSummary.Breakdown.ItemTotal.Currency, testInvoice.AmountSummary.Breakdown.ItemTotal.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.ItemTotal.Value, testInvoice.AmountSummary.Breakdown.ItemTotal.Value)
// amount-->breakdown-->shipping-->amount
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Amount.Currency, testInvoice.AmountSummary.Breakdown.Shipping.Amount.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Amount.Value, testInvoice.AmountSummary.Breakdown.Shipping.Amount.Value)
// amount-->breakdown-->shipping-->tax
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Tax.Amount.Currency, testInvoice.AmountSummary.Breakdown.Shipping.Tax.Amount.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Tax.Amount.Value, testInvoice.AmountSummary.Breakdown.Shipping.Tax.Amount.Value)
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Tax.ID, testInvoice.AmountSummary.Breakdown.Shipping.Tax.ID)
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Tax.Name, testInvoice.AmountSummary.Breakdown.Shipping.Tax.Name)
assert.Equal(t, invoice.AmountSummary.Breakdown.Shipping.Tax.Percent, testInvoice.AmountSummary.Breakdown.Shipping.Tax.Percent)
// amount-->breakdown-->tax_total
assert.Equal(t, invoice.AmountSummary.Breakdown.TaxTotal.Currency, testInvoice.AmountSummary.Breakdown.TaxTotal.Currency)
assert.Equal(t, invoice.AmountSummary.Breakdown.TaxTotal.Value, testInvoice.AmountSummary.Breakdown.TaxTotal.Value)
// configuration
assert.Equal(t, invoice.Configuration.AllowTip, testInvoice.Configuration.AllowTip)
assert.Equal(t, invoice.Configuration.TaxCalculatedAfterDiscount, testInvoice.Configuration.TaxCalculatedAfterDiscount)
assert.Equal(t, invoice.Configuration.TaxInclusive, testInvoice.Configuration.TaxInclusive)
assert.Equal(t, invoice.Configuration.TemplateId, testInvoice.Configuration.TemplateId)
// configuration --> partial_payment
assert.Equal(t, invoice.Configuration.PartialPayment.AllowPartialPayment, testInvoice.Configuration.PartialPayment.AllowPartialPayment)
assert.Equal(t, invoice.Configuration.PartialPayment.MinimumAmountDue.Currency, testInvoice.Configuration.PartialPayment.MinimumAmountDue.Currency)
assert.Equal(t, invoice.Configuration.PartialPayment.MinimumAmountDue.Value, testInvoice.Configuration.PartialPayment.MinimumAmountDue.Value)
// detail
assert.Equal(t, invoice.Detail.CurrencyCode, testInvoice.Detail.CurrencyCode)
assert.Equal(t, invoice.Detail.InvoiceDate, testInvoice.Detail.InvoiceDate)
assert.Equal(t, invoice.Detail.InvoiceNumber, testInvoice.Detail.InvoiceNumber)
assert.Equal(t, invoice.Detail.Memo, testInvoice.Detail.Memo)
assert.Equal(t, invoice.Detail.Note, testInvoice.Detail.Note)
assert.Equal(t, invoice.Detail.Reference, testInvoice.Detail.Reference)
assert.Equal(t, invoice.Detail.TermsAndConditions, testInvoice.Detail.TermsAndConditions)
// detail --> attachments !! EQUALITY OF SPLICE OF STRUCT REMAINING !!
assert.Equal(t, len(invoice.Detail.Attachments), len(testInvoice.Detail.Attachments))
// detail --> metadata
assert.Equal(t, invoice.Detail.Metadata.CancelTime, testInvoice.Detail.Metadata.CancelTime)
assert.Equal(t, invoice.Detail.Metadata.CancellledTimeBy, testInvoice.Detail.Metadata.CancellledTimeBy)
assert.Equal(t, invoice.Detail.Metadata.CreateTime, testInvoice.Detail.Metadata.CreateTime)
assert.Equal(t, invoice.Detail.Metadata.CreatedBy, testInvoice.Detail.Metadata.CreatedBy)
assert.Equal(t, invoice.Detail.Metadata.CreatedByFlow, testInvoice.Detail.Metadata.CreatedByFlow)
assert.Equal(t, invoice.Detail.Metadata.FirstSentTime, testInvoice.Detail.Metadata.FirstSentTime)
assert.Equal(t, invoice.Detail.Metadata.InvoicerViewUrl, testInvoice.Detail.Metadata.InvoicerViewUrl)
assert.Equal(t, invoice.Detail.Metadata.LastSentBy, testInvoice.Detail.Metadata.LastSentBy)
assert.Equal(t, invoice.Detail.Metadata.LastSentTime, testInvoice.Detail.Metadata.LastSentTime)
assert.Equal(t, invoice.Detail.Metadata.LastUpdateTime, testInvoice.Detail.Metadata.LastUpdateTime)
assert.Equal(t, invoice.Detail.Metadata.LastUpdatedBy, testInvoice.Detail.Metadata.LastUpdatedBy)
assert.Equal(t, invoice.Detail.Metadata.RecipientViewUrl, testInvoice.Detail.Metadata.RecipientViewUrl)
// detail --> payment_term
assert.Equal(t, invoice.Detail.PaymentTerm.DueDate, testInvoice.Detail.PaymentTerm.DueDate)
assert.Equal(t, invoice.Detail.PaymentTerm.TermType, testInvoice.Detail.PaymentTerm.TermType)
// due_amount
assert.Equal(t, invoice.DueAmount.Currency, testInvoice.DueAmount.Currency)
assert.Equal(t, invoice.DueAmount.Value, testInvoice.DueAmount.Value)
// gratuity
assert.Equal(t, invoice.Gratuity.Currency, testInvoice.Gratuity.Currency)
assert.Equal(t, invoice.Gratuity.Value, testInvoice.Gratuity.Value)
// id
assert.Equal(t, invoice.ID, testInvoice.ID)
// invoicer
assert.Equal(t, invoice.Invoicer.AdditionalNotes, testInvoice.Invoicer.AdditionalNotes)
assert.Equal(t, invoice.Invoicer.EmailAddress, testInvoice.Invoicer.EmailAddress)
assert.Equal(t, invoice.Invoicer.LogoUrl, testInvoice.Invoicer.LogoUrl)
assert.Equal(t, invoice.Invoicer.TaxId, testInvoice.Invoicer.TaxId)
assert.Equal(t, invoice.Invoicer.Website, testInvoice.Invoicer.Website)
// !!! SPLICE EQUALITY STILL REMAINING !!!!!
// invoicer --> phones
assert.Equal(t, len(invoice.Invoicer.Phones), len(testInvoice.Invoicer.Phones))
// items
// !!! SPLICE EQUALITY STILL REMAINING !!!!!
assert.Equal(t, len(invoice.Items), len(testInvoice.Items))
// links
// !!! SPLICE EQUALITY STILL REMAINING !!!!!
assert.Equal(t, len(invoice.Links), len(testInvoice.Links))
// parent_id
assert.Equal(t, invoice.ParentID, testInvoice.ParentID)
// payments
assert.Equal(t, invoice.Payments.PaidAmount.Currency, testInvoice.Payments.PaidAmount.Currency)
assert.Equal(t, invoice.Payments.PaidAmount.Value, testInvoice.Payments.PaidAmount.Value)
// payments --> transactions
assert.Equal(t, len(invoice.Payments.Transactions), len(testInvoice.Payments.Transactions))
// primary_recipients
// !!! SPLICE EQUALITY STILL REMAINING !!!!!
assert.Equal(t, len(invoice.PrimaryRecipients), len(testInvoice.PrimaryRecipients))
// refunds
assert.Equal(t, invoice.Refunds.RefundAmount.Currency, testInvoice.Refunds.RefundAmount.Currency)
assert.Equal(t, invoice.Refunds.RefundAmount.Value, testInvoice.Refunds.RefundAmount.Value)
assert.Equal(t, len(invoice.Refunds.RefundDetails), len(testInvoice.Refunds.RefundDetails))
// status
assert.Equal(t, invoice.Status, testInvoice.Status)
}
func TestGetInvoice(t *testing.T) {
testInvoiceJSONData := []byte(`
{
"amount": {
"breakdown": {
"custom": {
"amount": {
"currency_code": "USD",
"value": "10.00"
},
"label": "Packing Charges"
},
"discount": {
"invoice_discount": {
"amount": {
"currency_code": "USD",
"value": "-2.63"
},
"percent": "5"
},
"item_discount": {
"currency_code": "USD",
"value": "-7.50"
}
},
"item_total": {
"currency_code": "USD",
"value": "60.00"
},
"shipping": {
"amount": {
"currency_code": "USD",
"value": "10.00"
},
"tax": {
"amount": {
"currency_code": "USD",
"value": "0.73"
},
"id": "TAX-5XV24702TP4910056",
"name": "Sales Tax",
"percent": "7.25"
}
},
"tax_total": {
"currency_code": "USD",
"value": "4.34"
}
},
"currency_code": "USD",
"value": "74.21"
},
"configuration": {
"allow_tip": true,
"partial_payment": {
"allow_partial_payment": true,
"minimum_amount_due": {
"currency_code": "USD",
"value": "20.00"
}
},
"tax_calculated_after_discount": true,
"tax_inclusive": false,
"template_id": "TEMP-4NW98229SC0703920"
},
"detail": {
"additional_data": "2-4",
"archived": false,
"category_code": "SHIPPABLE",
"currency_code": "USD",
"group_draft": false,
"invoice_date": "2018-11-12",
"invoice_number": "0001",
"memo": "This is a long contract",
"metadata": {
"caller_type": "API_V2_INVOICE",
"create_time": "2022-10-25T16:54:50Z",
"created_by_flow": "REGULAR_SINGLE",
"invoicer_view_url": "https://www.sandbox.paypal.com/invoice/details/INV2-XFXV-YW42-ZANU-4F33",
"last_update_time": "2022-10-25T16:54:50Z",
"recipient_view_url": "https://www.sandbox.paypal.com/invoice/p/#XFXVYW42ZANU4F33"
},
"note": "Thank you for your business.",
"payment_term": {
"due_date": "2018-11-22",
"term_type": "NET_10"
},
"reference": "deal-ref",
"viewed_by_recipient": false
},
"due_amount": {
"currency_code": "USD",
"value": "74.21"
},
"id": "INV2-XFXV-YW42-ZANU-4F33",
"invoicer": {
"additional_notes": "2-4",
"address": {
"address_line_1": "1234 First Street",
"address_line_2": "337673 Hillside Court",
"admin_area_1": "CA",
"admin_area_2": "Anytown",
"country_code": "US",
"postal_code": "98765"
},
"email_address": "merchant@example.com",
"logo_url": "https://example.com/logo.PNG",
"name": {
"full_name": "David Larusso",
"given_name": "David",
"surname": "Larusso"
},
"phones": [
{
"country_code": "001",
"national_number": "4085551234",
"phone_type": "MOBILE"
}
],
"tax_id": "ABcNkWSfb5ICTt73nD3QON1fnnpgNKBy- Jb5SeuGj185MNNw6g",
"website": "www.test.com"
},
"items": [
{
"description": "Elastic mat to practice yoga.",
"discount": {
"amount": {
"currency_code": "USD",
"value": "-2.50"
},
"percent": "5"
},
"id": "ITEM-5335764681676603X",
"name": "Yoga Mat",
"quantity": "1",
"tax": {
"amount": {
"currency_code": "USD",
"value": "3.27"
},
"id": "TAX-5XV24702TP4910056",
"name": "Sales Tax",
"percent": "7.25"
},
"unit_amount": {
"currency_code": "USD",
"value": "50.00"
},
"unit_of_measure": "QUANTITY"
},
{
"discount": {
"amount": {
"currency_code": "USD",
"value": "-5.00"
}
},
"id": "ITEM-1B467958Y9218273X",
"name": "Yoga t-shirt",
"quantity": "1",
"tax": {
"amount": {
"currency_code": "USD",
"value": "0.34"
},
"id": "TAX-5XV24702TP4910056",
"name": "Sales Tax",
"percent": "7.25"
},
"unit_amount": {
"currency_code": "USD",
"value": "10.00"
},
"unit_of_measure": "QUANTITY"
}
],
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-XFXV-YW42-ZANU-4F33",
"method": "GET",
"rel": "self"
},
{
"href": "https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-XFXV-YW42-ZANU-4F33/send",
"method": "POST",
"rel": "send"
},
{
"href": "https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-XFXV-YW42-ZANU-4F33",
"method": "PUT",
"rel": "replace"
},
{
"href": "https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-XFXV-YW42-ZANU-4F33",
"method": "DELETE",
"rel": "delete"
},
{
"href": "https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-XFXV-YW42-ZANU-4F33/payments",
"method": "POST",
"rel": "record-payment"
}
],
"primary_recipients": [
{
"billing_info": {
"address": {
"address_line_1": "1234 Main Street",
"admin_area_1": "CA",
"admin_area_2": "Anytown",
"country_code": "US",
"postal_code": "98765"
},
"email_address": "bill-me@example.com",
"name": {
"full_name": "Stephanie Meyers",
"given_name": "Stephanie",
"surname": "Meyers"
}
},
"shipping_info": {
"address": {
"address_line_1": "1234 Main Street",
"admin_area_1": "CA",
"admin_area_2": "Anytown",
"country_code": "US",
"postal_code": "98765"
},
"name": {
"full_name": "Stephanie Meyers",
"given_name": "Stephanie",
"surname": "Meyers"
}
}
}
],
"status": "DRAFT",
"unilateral": false
}
`)
var testInvoice paypal.Invoice
err := json.Unmarshal(testInvoiceJSONData, &testInvoice)
assert.Equal(t, nil, err) // if passed, means unmarshalling was successful
ctx := context.Background()
c, _ := paypal.NewClient(devTestClientID, devTestSecret, devAPIBaseSandBox)
_, _ = c.GetAccessToken(ctx)
invoice, err := c.GetInvoiceDetails(ctx, "INV2-XFXV-YW42-ZANU-4F33")
assert.Equal(t, nil, err) // if passed, means that request was successful
assertTwoInvoices(t, *invoice, testInvoice)
}

191
order.go
View File

@ -1,13 +1,17 @@
package paypalsdk
package paypal
import "fmt"
import (
"context"
"encoding/json"
"fmt"
)
// GetOrder retrieves order by ID
// Endpoint: GET /v1/payments/orders/ID
func (c *Client) GetOrder(orderID string) (*Order, error) {
// Endpoint: GET /v2/checkout/orders/ID
func (c *Client) GetOrder(ctx context.Context, orderID string) (*Order, error) {
order := &Order{}
req, err := c.NewRequest("GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v1/payments/orders/", orderID), nil)
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s%s", c.APIBase, "/v2/checkout/orders/", orderID), nil)
if err != nil {
return order, err
}
@ -19,16 +23,79 @@ func (c *Client) GetOrder(orderID string) (*Order, error) {
return order, nil
}
// AuthorizeOrder - Use this call to authorize an order.
// Endpoint: POST /v1/payments/orders/ID/authorize
func (c *Client) AuthorizeOrder(orderID string, amount *Amount) (*Authorization, error) {
type authRequest struct {
Amount *Amount `json:"amount"`
// Create an order
// Endpoint: POST /v2/checkout/orders
func (c *Client) CreateOrder(ctx context.Context, intent string, purchaseUnits []PurchaseUnitRequest, paymentSource *PaymentSource, appContext *ApplicationContext) (*Order, error) {
return c.CreateOrderWithPaypalRequestID(ctx, intent, purchaseUnits, paymentSource, appContext, "")
}
// CreateOrderWithPaypalRequestID - Use this call to create an order with idempotency
// Endpoint: POST /v2/checkout/orders
func (c *Client) CreateOrderWithPaypalRequestID(ctx context.Context,
intent string,
purchaseUnits []PurchaseUnitRequest,
paymentSource *PaymentSource,
appContext *ApplicationContext,
requestID string,
) (*Order, error) {
type createOrderRequest struct {
Intent string `json:"intent"`
PaymentSource *PaymentSource `json:"payment_source,omitempty"`
PurchaseUnits []PurchaseUnitRequest `json:"purchase_units"`
ApplicationContext *ApplicationContext `json:"application_context,omitempty"`
}
auth := &Authorization{}
order := &Order{}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/orders/"+orderID+"/authorize"), authRequest{Amount: amount})
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/checkout/orders"), createOrderRequest{Intent: intent, PurchaseUnits: purchaseUnits, PaymentSource: paymentSource, ApplicationContext: appContext})
if err != nil {
return order, err
}
if requestID != "" {
req.Header.Set("PayPal-Request-Id", requestID)
}
if err = c.SendWithAuth(req, order); err != nil {
return order, err
}
return order, nil
}
// UpdateOrder updates the order by ID
// Endpoint: PATCH /v2/checkout/orders/ID
func (c *Client) UpdateOrder(ctx context.Context, orderID string, op string, path string, value map[string]string) error {
type patchRequest struct {
Op string `json:"op"`
Path string `json:"path"`
Value map[string]string `json:"value"`
}
req, err := c.NewRequest(ctx, "PATCH", fmt.Sprintf("%s%s%s", c.APIBase, "/v2/checkout/orders/", orderID), []patchRequest{
{
Op: op,
Path: path,
Value: value,
},
})
if err != nil {
return err
}
if err = c.SendWithAuth(req, nil); err != nil {
return err
}
return nil
}
// AuthorizeOrder - https://developer.paypal.com/docs/api/orders/v2/#orders_authorize
// Endpoint: POST /v2/checkout/orders/ID/authorize
func (c *Client) AuthorizeOrder(ctx context.Context, orderID string, authorizeOrderRequest AuthorizeOrderRequest) (*AuthorizeOrderResponse, error) {
auth := &AuthorizeOrderResponse{}
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/checkout/orders/"+orderID+"/authorize"), authorizeOrderRequest)
if err != nil {
return auth, err
}
@ -40,22 +107,42 @@ func (c *Client) AuthorizeOrder(orderID string, amount *Amount) (*Authorization,
return auth, nil
}
// CaptureOrder - Use this call to capture a payment on an order. To use this call, an original payment call must specify an intent of order.
// Endpoint: POST /v1/payments/orders/ID/capture
func (c *Client) CaptureOrder(orderID string, amount *Amount, isFinalCapture bool, currency *Currency) (*Capture, error) {
type captureRequest struct {
Amount *Amount `json:"amount"`
IsFinalCapture bool `json:"is_final_capture"`
Currency *Currency `json:"transaction_fee"`
}
// CaptureOrder - https://developer.paypal.com/docs/api/orders/v2/#orders_capture
// Endpoint: POST /v2/checkout/orders/ID/capture
func (c *Client) CaptureOrder(ctx context.Context, orderID string, captureOrderRequest CaptureOrderRequest) (*CaptureOrderResponse, error) {
return c.CaptureOrderWithPaypalRequestId(ctx, orderID, captureOrderRequest, "", nil)
}
capture := &Capture{}
// CaptureOrder with idempotency - https://developer.paypal.com/docs/api/orders/v2/#orders_capture
// Endpoint: POST /v2/checkout/orders/ID/capture
// https://developer.paypal.com/docs/api/reference/api-requests/#http-request-headers
func (c *Client) CaptureOrderWithPaypalRequestId(ctx context.Context,
orderID string,
captureOrderRequest CaptureOrderRequest,
requestID string,
mockResponse *CaptureOrderMockResponse,
) (*CaptureOrderResponse, error) {
capture := &CaptureOrderResponse{}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/orders/"+orderID+"/capture"), captureRequest{Amount: amount, IsFinalCapture: isFinalCapture, Currency: currency})
c.SetReturnRepresentation()
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/checkout/orders/"+orderID+"/capture"), captureOrderRequest)
if err != nil {
return capture, err
}
if requestID != "" {
req.Header.Set("PayPal-Request-Id", requestID)
}
if mockResponse != nil {
mock, err := json.Marshal(mockResponse)
if err != nil {
return nil, err
}
req.Header.Set("PayPal-Mock-Response", string(mock))
}
if err = c.SendWithAuth(req, capture); err != nil {
return capture, err
}
@ -63,20 +150,48 @@ func (c *Client) CaptureOrder(orderID string, amount *Amount, isFinalCapture boo
return capture, nil
}
// VoidOrder - Use this call to void an existing order.
// Note: An order cannot be voided if payment has already been partially or fully captured.
// Endpoint: POST /v1/payments/orders/ID/do-void
func (c *Client) VoidOrder(orderID string) (*Order, error) {
order := &Order{}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/orders/"+orderID+"/do-void"), nil)
if err != nil {
return order, err
}
if err = c.SendWithAuth(req, order); err != nil {
return order, err
}
return order, nil
// RefundCapture - https://developer.paypal.com/docs/api/payments/v2/#captures_refund
// Endpoint: POST /v2/payments/captures/ID/refund
func (c *Client) RefundCapture(ctx context.Context, captureID string, refundCaptureRequest RefundCaptureRequest) (*RefundResponse, error) {
return c.RefundCaptureWithPaypalRequestId(ctx, captureID, refundCaptureRequest, "")
}
// RefundCapture with idempotency - https://developer.paypal.com/docs/api/payments/v2/#captures_refund
// Endpoint: POST /v2/payments/captures/ID/refund
func (c *Client) RefundCaptureWithPaypalRequestId(ctx context.Context,
captureID string,
refundCaptureRequest RefundCaptureRequest,
requestID string,
) (*RefundResponse, error) {
refund := &RefundResponse{}
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v2/payments/captures/"+captureID+"/refund"), refundCaptureRequest)
if err != nil {
return refund, err
}
if requestID != "" {
req.Header.Set("PayPal-Request-Id", requestID)
}
if err = c.SendWithAuth(req, refund); err != nil {
return refund, err
}
return refund, nil
}
// CapturedDetail - https://developer.paypal.com/docs/api/payments/v2/#captures_get
// Endpoint: GET /v2/payments/captures/ID
func (c *Client) CapturedDetail(ctx context.Context, captureID string) (*CaptureDetailsResponse, error) {
response := &CaptureDetailsResponse{}
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s", c.APIBase, "/v2/payments/captures/"+captureID), nil)
if err != nil {
return response, err
}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}
return response, nil
}

View File

@ -1,146 +0,0 @@
package paypalsdk
import (
"bytes"
"errors"
"fmt"
"net/http"
)
// ListPaymentsResp slice of payments
type ListPaymentsResp struct {
Payments []Payment `json:"payments"`
}
// CreatePaymentResp contains Payment Info and Links slice
type CreatePaymentResp struct {
*Payment
Links []Link `json:"links"`
}
// CreateDirectPaypalPayment sends request to create a payment with payment_method=paypal
// CreatePayment is more common function for any kind of payment
// Endpoint: POST /v1/payments/payment
func (c *Client) CreateDirectPaypalPayment(amount Amount, redirectURI string, cancelURI string, description string) (*PaymentResponse, error) {
buf := bytes.NewBuffer([]byte("{\"intent\":\"sale\",\"payer\":{\"payment_method\":\"paypal\"}," +
"\"transactions\":[{\"amount\":{\"total\":\"" + amount.Total +
"\",\"currency\":\"" + amount.Currency + "\"},\"description\":\"" + description + "\"}],\"redirect_urls\":{\"return_url\":\"" +
redirectURI + "\",\"cancel_url\":\"" + cancelURI + "\"}}"))
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payment"), buf)
if err != nil {
return &PaymentResponse{}, err
}
req.SetBasicAuth(c.ClientID, c.Secret)
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
p := PaymentResponse{}
if err = c.SendWithAuth(req, &p); err != nil {
return &p, err
}
if p.ID == "" {
return &p, errors.New("Unable to create payment with this access token")
}
return &p, err
}
// CreatePayment creates a payment in Paypal
// Depending on the payment_method and the funding_instrument, you can use the payment resource for direct credit card payments, stored credit card payments, or PayPal account payments.
// Endpoint: POST /v1/payments/payment
func (c *Client) CreatePayment(p Payment) (*CreatePaymentResp, error) {
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payment"), p)
if err != nil {
return &CreatePaymentResp{}, err
}
response := &CreatePaymentResp{}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}
return response, nil
}
// ExecuteApprovedPayment - Use this call to execute (complete) a PayPal payment that has been approved by the payer. You can optionally update transaction information when executing the payment by passing in one or more transactions.
// Endpoint: POST /v1/payments/payment/paymentID/execute
func (c *Client) ExecuteApprovedPayment(paymentID string, payerID string) (*ExecuteResponse, error) {
buf := bytes.NewBuffer([]byte("{\"payer_id\":\"" + payerID + "\"}"))
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payment/"+paymentID+"/execute"), buf)
if err != nil {
return &ExecuteResponse{}, err
}
req.SetBasicAuth(c.ClientID, c.Secret)
req.Header.Set("Authorization", "Bearer "+c.Token.Token)
e := ExecuteResponse{}
if err = c.SendWithAuth(req, &e); err != nil {
return &e, err
}
if e.ID == "" {
return &e, errors.New("Unable to execute payment with paymentID=" + paymentID)
}
return &e, err
}
// GetPayment gets a payment from PayPal
// Endpoint: GET /v1/payments/payment/ID
func (c *Client) GetPayment(paymentID string) (*Payment, error) {
p := Payment{}
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payment/"+paymentID), nil)
if err != nil {
return &p, err
}
if err = c.SendWithAuth(req, &p); err != nil {
return &p, err
}
if p.ID == "" {
return &p, errors.New("Unable to get payment with paymentID=" + paymentID)
}
return &p, nil
}
// PatchPayment modifies some fields of a payment prior to execution
// Endpoint: PATCH /v1/payments/payment/ID
func (c *Client) PatchPayment(paymentID string, p []PaymentPatch) (*Payment, error) {
req, err := c.NewRequest("PATCH", fmt.Sprintf("%s/v1/payments/payment/%s", c.APIBase, paymentID), p)
if err != nil {
return nil, err
}
response := Payment{}
if err = c.SendWithAuth(req, &response); err != nil {
return nil, err
}
return &response, nil
}
// GetPayments retrieve payments resources from Paypal
// Endpoint: GET /v1/payments/payment/
func (c *Client) GetPayments() ([]Payment, error) {
var p ListPaymentsResp
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payment/"), nil)
if err != nil {
return p.Payments, err
}
if err = c.SendWithAuth(req, &p); err != nil {
return p.Payments, err
}
return p.Payments, nil
}

View File

@ -1,20 +1,21 @@
package paypalsdk
package paypal
import (
"context"
"fmt"
)
// CreateSinglePayout submits a payout with an asynchronous API call, which immediately returns the results of a PayPal payment.
// CreatePayout submits a payout with an asynchronous API call, which immediately returns the results of a PayPal payment.
// For email payout set RecipientType: "EMAIL" and receiver email into Receiver
// Endpoint: POST /v1/payments/payouts
func (c *Client) CreateSinglePayout(p Payout) (*PayoutResponse, error) {
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts"), p)
if err != nil {
return &PayoutResponse{}, err
}
func (c *Client) CreatePayout(ctx context.Context, p Payout) (*PayoutResponse, error) {
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts"), p)
response := &PayoutResponse{}
if err != nil {
return response, err
}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}
@ -25,15 +26,14 @@ func (c *Client) CreateSinglePayout(p Payout) (*PayoutResponse, error) {
// GetPayout shows the latest status of a batch payout along with the transaction status and other data for individual items.
// Also, returns IDs for the individual payout items. You can use these item IDs in other calls.
// Endpoint: GET /v1/payments/payouts/ID
func (c *Client) GetPayout(payoutBatchID string) (*PayoutResponse, error) {
req, err := c.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts/"+payoutBatchID), nil)
func (c *Client) GetPayout(ctx context.Context, payoutBatchID string) (*PayoutResponse, error) {
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts/"+payoutBatchID), nil)
response := &PayoutResponse{}
if err != nil {
return &PayoutResponse{}, err
return response, err
}
response := &PayoutResponse{}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}
@ -44,15 +44,14 @@ func (c *Client) GetPayout(payoutBatchID string) (*PayoutResponse, error) {
// GetPayoutItem shows the details for a payout item.
// Use this call to review the current status of a previously unclaimed, or pending, payout item.
// Endpoint: GET /v1/payments/payouts-item/ID
func (c *Client) GetPayoutItem(payoutItemID string) (*PayoutItemResponse, error) {
req, err := c.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts-item/"+payoutItemID), nil)
func (c *Client) GetPayoutItem(ctx context.Context, payoutItemID string) (*PayoutItemResponse, error) {
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts-item/"+payoutItemID), nil)
response := &PayoutItemResponse{}
if err != nil {
return &PayoutItemResponse{}, err
return response, err
}
response := &PayoutItemResponse{}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}
@ -63,15 +62,14 @@ func (c *Client) GetPayoutItem(payoutItemID string) (*PayoutItemResponse, error)
// CancelPayoutItem cancels an unclaimed Payout Item. If no one claims the unclaimed item within 30 days,
// the funds are automatically returned to the sender. Use this call to cancel the unclaimed item before the automatic 30-day refund.
// Endpoint: POST /v1/payments/payouts-item/ID/cancel
func (c *Client) CancelPayoutItem(payoutItemID string) (*PayoutItemResponse, error) {
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts-item/"+payoutItemID+"/cancel"), nil)
func (c *Client) CancelPayoutItem(ctx context.Context, payoutItemID string) (*PayoutItemResponse, error) {
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/payouts-item/"+payoutItemID+"/cancel"), nil)
response := &PayoutItemResponse{}
if err != nil {
return &PayoutItemResponse{}, err
return response, err
}
response := &PayoutItemResponse{}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}

119
products.go Normal file
View File

@ -0,0 +1,119 @@
package paypal
import (
"context"
"fmt"
"net/http"
)
type (
// Product struct
Product struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Category ProductCategory `json:"category,omitempty"`
Type ProductType `json:"type"`
ImageUrl string `json:"image_url,omitempty"`
HomeUrl string `json:"home_url,omitempty"`
}
CreateProductResponse struct {
Product
SharedResponse
}
ListProductsResponse struct {
Products []Product `json:"products"`
SharedListResponse
}
ProductListParameters struct {
ListParams
}
)
func (p *Product) GetUpdatePatch() []Patch {
return []Patch{
{
Operation: "replace",
Path: "/description",
Value: p.Description,
},
{
Operation: "replace",
Path: "/category",
Value: p.Category,
},
{
Operation: "replace",
Path: "/image_url",
Value: p.ImageUrl,
},
{
Operation: "replace",
Path: "/home_url",
Value: p.HomeUrl,
},
}
}
// CreateProduct creates a product
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_create
// Endpoint: POST /v1/catalogs/products
func (c *Client) CreateProduct(ctx context.Context, product Product) (*CreateProductResponse, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/catalogs/products"), product)
response := &CreateProductResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// UpdateProduct. updates a product information
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_patch
// Endpoint: PATCH /v1/catalogs/products/:product_id
func (c *Client) UpdateProduct(ctx context.Context, product Product) error {
req, err := c.NewRequest(ctx, http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/catalogs/products/", product.ID), product.GetUpdatePatch())
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Get product details
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_get
// Endpoint: GET /v1/catalogs/products/:product_id
func (c *Client) GetProduct(ctx context.Context, productId string) (*Product, error) {
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/catalogs/products/", productId), nil)
response := &Product{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// List all products
// Doc: https://developer.paypal.com/docs/api/catalog-products/v1/#products_list
// Endpoint: GET /v1/catalogs/products
func (c *Client) ListProducts(ctx context.Context, params *ProductListParameters) (*ListProductsResponse, error) {
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s", c.APIBase, "/v1/catalogs/products"), nil)
response := &ListProductsResponse{}
if err != nil {
return response, err
}
if params != nil {
q := req.URL.Query()
q.Add("page", params.Page)
q.Add("page_size", params.PageSize)
q.Add("total_required", params.TotalRequired)
req.URL.RawQuery = q.Encode()
}
err = c.SendWithAuth(req, response)
return response, err
}

54
sale.go
View File

@ -1,55 +1,17 @@
package paypalsdk
package paypal
import "fmt"
// GetSale returns a sale by ID
// Use this call to get details about a sale transaction.
// Note: This call returns only the sales that were created via the REST API.
// Endpoint: GET /v1/payments/sale/ID
func (c *Client) GetSale(saleID string) (*Sale, error) {
sale := &Sale{}
req, err := c.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/sale/"+saleID), nil)
if err != nil {
return sale, err
}
if err = c.SendWithAuth(req, sale); err != nil {
return sale, err
}
return sale, nil
}
// RefundSale refunds a completed payment.
// Use this call to refund a completed payment. Provide the sale_id in the URI and an empty JSON payload for a full refund. For partial refunds, you can include an amount.
// Endpoint: POST /v1/payments/sale/ID/refund
func (c *Client) RefundSale(saleID string, a *Amount) (*Refund, error) {
type refundRequest struct {
Amount *Amount `json:"amount"`
}
refund := &Refund{}
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/sale/"+saleID+"/refund"), &refundRequest{Amount: a})
if err != nil {
return refund, err
}
if err = c.SendWithAuth(req, refund); err != nil {
return refund, err
}
return refund, nil
}
import (
"context"
"fmt"
)
// GetRefund by ID
// Use it to look up details of a specific refund on direct and captured payments.
// Endpoint: GET /v1/payments/refund/ID
func (c *Client) GetRefund(refundID string) (*Refund, error) {
// Endpoint: GET /v2/payments/refund/ID
func (c *Client) GetRefund(ctx context.Context, refundID string) (*Refund, error) {
refund := &Refund{}
req, err := c.NewRequest("GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/payments/refund/"+refundID), nil)
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s", c.APIBase, "/v2/payments/refund/"+refundID), nil)
if err != nil {
return refund, err
}

234
subscription.go Normal file
View File

@ -0,0 +1,234 @@
package paypal
import (
"context"
"fmt"
"net/http"
"time"
)
type (
SubscriptionBase struct {
PlanID string `json:"plan_id"`
StartTime *JSONTime `json:"start_time,omitempty"`
Quantity string `json:"quantity,omitempty"`
ShippingAmount *Money `json:"shipping_amount,omitempty"`
Subscriber *Subscriber `json:"subscriber,omitempty"`
AutoRenewal bool `json:"auto_renewal,omitempty"`
ApplicationContext *ApplicationContext `json:"application_context,omitempty"`
CustomID string `json:"custom_id,omitempty"`
Plan *PlanOverride `json:"plan,omitempty"`
}
SubscriptionDetails struct {
ID string `json:"id,omitempty"`
SubscriptionStatus SubscriptionStatus `json:"status,omitempty"`
SubscriptionStatusChangeNote string `json:"status_change_note,omitempty"`
StatusUpdateTime time.Time `json:"status_update_time,omitempty"`
}
Subscription struct {
SubscriptionDetailResp
}
// SubscriptionDetailResp struct
SubscriptionDetailResp struct {
SubscriptionBase
SubscriptionDetails
BillingInfo BillingInfo `json:"billing_info,omitempty"` // not found in documentation
SharedResponse
}
SubscriptionCaptureResponse struct {
Status SubscriptionTransactionStatus `json:"status"`
Id string `json:"id"`
AmountWithBreakdown AmountWithBreakdown `json:"amount_with_breakdown"`
PayerName Name `json:"payer_name"`
PayerEmail string `json:"payer_email"`
Time time.Time `json:"time"`
}
//Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-amount_with_breakdown
AmountWithBreakdown struct {
GrossAmount Money `json:"gross_amount"`
FeeAmount Money `json:"fee_amount"`
ShippingAmount Money `json:"shipping_amount"`
TaxAmount Money `json:"tax_amount"`
NetAmount Money `json:"net_amount"`
}
SubscriptionTransactionsParams struct {
SubscriptionId string
StartTime time.Time
EndTime time.Time
}
SubscriptionTransactionsResponse struct {
Transactions []SubscriptionCaptureResponse `json:"transactions"`
SharedListResponse
}
CaptureRequest struct {
Note string `json:"note"`
CaptureType CaptureType `json:"capture_type"`
Amount Money `json:"amount"`
}
// https://developer.paypal.com/docs/api/subscriptions/v1/#definition-plan_override
PlanOverride struct {
BillingCycles []BillingCycleOverride `json:"billing_cycles,omitempty"`
PaymentPreferences *PaymentPreferencesOverride `json:"payment_preferences,omitempty"`
Taxes *TaxesOverride `json:"taxes,omitempty"`
}
// https://developer.paypal.com/docs/api/subscriptions/v1/#definition-payment_preferences_override
PaymentPreferencesOverride struct {
AutoBillOutstanding bool `json:"auto_bill_outstanding,omitempty"`
SetupFee Money `json:"setup_fee,omitempty"`
SetupFeeFailureAction SetupFeeFailureAction `json:"setup_fee_failure_action,omitempty"`
PaymentFailureThreshold int `json:"payment_failure_threshold,omitempty"`
}
// https://developer.paypal.com/docs/api/subscriptions/v1/#definition-payment_preferences_override
TaxesOverride struct {
Percentage string `json:"percentage,omitempty"`
Inclusive *bool `json:"inclusive,omitempty"`
}
// https://developer.paypal.com/docs/api/subscriptions/v1/#definition-billing_cycle_override
BillingCycleOverride struct {
PricingScheme PricingScheme `json:"pricing_scheme,omitempty"`
Sequence *int `json:"sequence,omitempty"`
TotalCycles *int `json:"total_cycles,omitempty"`
}
)
func (p *Subscription) GetUpdatePatch() []Patch {
result := []Patch{
{
Operation: "replace",
Path: "/billing_info/outstanding_balance",
Value: p.BillingInfo.OutstandingBalance,
},
}
return result
}
// CreateSubscriptionPlan creates a subscriptionPlan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_create
// Endpoint: POST /v1/billing/subscriptions
func (c *Client) CreateSubscription(ctx context.Context, newSubscription SubscriptionBase) (*SubscriptionDetailResp, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/billing/subscriptions"), newSubscription)
req.Header.Add("Prefer", "return=representation")
response := &SubscriptionDetailResp{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// UpdateSubscriptionPlan. updates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_patch
// Endpoint: PATCH /v1/billing/subscriptions/:subscription_id
func (c *Client) UpdateSubscription(ctx context.Context, updatedSubscription Subscription) error {
req, err := c.NewRequest(ctx, http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/billing/subscriptions/", updatedSubscription.ID), updatedSubscription.GetUpdatePatch())
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// GetSubscriptionDetails shows details for a subscription, by ID.
// Endpoint: GET /v1/billing/subscriptions/
func (c *Client) GetSubscriptionDetails(ctx context.Context, subscriptionID string) (*SubscriptionDetailResp, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/billing/subscriptions/%s", c.APIBase, subscriptionID), nil)
response := &SubscriptionDetailResp{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// Activates the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_activate
// Endpoint: POST /v1/billing/subscriptions/{id}/activate
func (c *Client) ActivateSubscription(ctx context.Context, subscriptionId, activateReason string) error {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/activate", c.APIBase, subscriptionId), map[string]string{"reason": activateReason})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Cancels the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_cancel
// Endpoint: POST /v1/billing/subscriptions/{id}/cancel
func (c *Client) CancelSubscription(ctx context.Context, subscriptionId, cancelReason string) error {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/cancel", c.APIBase, subscriptionId), map[string]string{"reason": cancelReason})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Captures an authorized payment from the subscriber on the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_capture
// Endpoint: POST /v1/billing/subscriptions/{id}/capture
func (c *Client) CaptureSubscription(ctx context.Context, subscriptionId string, request CaptureRequest) (*SubscriptionCaptureResponse, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/capture", c.APIBase, subscriptionId), request)
response := &SubscriptionCaptureResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// Suspends the subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_suspend
// Endpoint: POST /v1/billing/subscriptions/{id}/suspend
func (c *Client) SuspendSubscription(ctx context.Context, subscriptionId, reason string) error {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/suspend", c.APIBase, subscriptionId), map[string]string{"reason": reason})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Lists transactions for a subscription.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_transactions
// Endpoint: GET /v1/billing/subscriptions/{id}/transactions
func (c *Client) GetSubscriptionTransactions(ctx context.Context, requestParams SubscriptionTransactionsParams) (*SubscriptionTransactionsResponse, error) {
startTime := requestParams.StartTime.Format("2006-01-02T15:04:05Z")
endTime := requestParams.EndTime.Format("2006-01-02T15:04:05Z")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v1/billing/subscriptions/%s/transactions?start_time=%s&end_time=%s", c.APIBase, requestParams.SubscriptionId, startTime, endTime), nil)
response := &SubscriptionTransactionsResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// Revise plan or quantity of subscription
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_revise
// Endpoint: POST /v1/billing/subscriptions/{id}/revise
func (c *Client) ReviseSubscription(ctx context.Context, subscriptionId string, reviseSubscription SubscriptionBase) (*SubscriptionDetailResp, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/subscriptions/%s/revise", c.APIBase, subscriptionId), reviseSubscription)
response := &SubscriptionDetailResp{}
if err != nil {
return response, err
}
req.Header.Add("Content-Type", "application/json")
err = c.SendWithAuth(req, response)
return response, err
}

233
subscription_plan.go Normal file
View File

@ -0,0 +1,233 @@
package paypal
import (
"context"
"fmt"
"net/http"
"time"
)
type (
// SubscriptionDetailResp struct
SubscriptionPlan struct {
ID string `json:"id,omitempty"`
ProductId string `json:"product_id"`
Name string `json:"name"`
Status SubscriptionPlanStatus `json:"status"`
Description string `json:"description,omitempty"`
BillingCycles []BillingCycle `json:"billing_cycles"`
PaymentPreferences *PaymentPreferences `json:"payment_preferences"`
Taxes *Taxes `json:"taxes"`
QuantitySupported bool `json:"quantity_supported"` //Indicates whether you can subscribe to this plan by providing a quantity for the goods or service.
}
// Doc https://developer.paypal.com/docs/api/subscriptions/v1/#definition-billing_cycle
BillingCycle struct {
PricingScheme PricingScheme `json:"pricing_scheme"` // The active pricing scheme for this billing cycle. A free trial billing cycle does not require a pricing scheme.
Frequency Frequency `json:"frequency"` // The frequency details for this billing cycle.
TenureType TenureType `json:"tenure_type"` // The tenure type of the billing cycle. In case of a plan having trial cycle, only 2 trial cycles are allowed per plan. The possible values are:
Sequence int `json:"sequence"` // The order in which this cycle is to run among other billing cycles. For example, a trial billing cycle has a sequence of 1 while a regular billing cycle has a sequence of 2, so that trial cycle runs before the regular cycle.
TotalCycles int `json:"total_cycles"` // The number of times this billing cycle gets executed. Trial billing cycles can only be executed a finite number of times (value between 1 and 999 for total_cycles). Regular billing cycles can be executed infinite times (value of 0 for total_cycles) or a finite number of times (value between 1 and 999 for total_cycles).
}
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-payment_preferences
PaymentPreferences struct {
AutoBillOutstanding bool `json:"auto_bill_outstanding"`
SetupFee *Money `json:"setup_fee"`
SetupFeeFailureAction SetupFeeFailureAction `json:"setup_fee_failure_action"`
PaymentFailureThreshold int `json:"payment_failure_threshold"`
}
PricingScheme struct {
Version int `json:"version"`
FixedPrice Money `json:"fixed_price"`
CreateTime time.Time `json:"create_time"`
UpdateTime time.Time `json:"update_time"`
}
PricingSchemeUpdateRequest struct {
Schemes []PricingSchemeUpdate `json:"pricing_schemes"`
}
PricingSchemeUpdate struct {
BillingCycleSequence int `json:"billing_cycle_sequence"`
PricingScheme PricingScheme `json:"pricing_scheme"`
}
//doc: https://developer.paypal.com/docs/api/subscriptions/v1/#definition-frequency
Frequency struct {
IntervalUnit IntervalUnit `json:"interval_unit"`
IntervalCount int `json:"interval_count"` //different per unit. check documentation
}
Taxes struct {
Percentage string `json:"percentage"`
Inclusive bool `json:"inclusive"`
}
CreateSubscriptionPlanResponse struct {
SubscriptionPlan
SharedResponse
}
SubscriptionPlanListParameters struct {
ProductId string `json:"product_id"`
PlanIds string `json:"plan_ids"` // Filters the response by list of plan IDs. Filter supports upto 10 plan IDs.
ListParams
}
ListSubscriptionPlansResponse struct {
Plans []SubscriptionPlan `json:"plans"`
SharedListResponse
}
)
func (p *SubscriptionPlan) GetUpdatePatch() []Patch {
result := []Patch{
{
Operation: "replace",
Path: "/description",
Value: p.Description,
},
}
if p.Taxes != nil {
result = append(result, Patch{
Operation: "replace",
Path: "/taxes/percentage",
Value: p.Taxes.Percentage,
})
}
if p.PaymentPreferences != nil {
if p.PaymentPreferences.SetupFee != nil {
result = append(result, Patch{
Operation: "replace",
Path: "/payment_preferences/setup_fee",
Value: p.PaymentPreferences.SetupFee,
},
)
}
result = append(result, []Patch{{
Operation: "replace",
Path: "/payment_preferences/auto_bill_outstanding",
Value: p.PaymentPreferences.AutoBillOutstanding,
},
{
Operation: "replace",
Path: "/payment_preferences/payment_failure_threshold",
Value: p.PaymentPreferences.PaymentFailureThreshold,
},
{
Operation: "replace",
Path: "/payment_preferences/setup_fee_failure_action",
Value: p.PaymentPreferences.SetupFeeFailureAction,
}}...)
}
return result
}
// CreateSubscriptionPlan creates a subscriptionPlan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_create
// Endpoint: POST /v1/billing/plans
func (c *Client) CreateSubscriptionPlan(ctx context.Context, newPlan SubscriptionPlan) (*CreateSubscriptionPlanResponse, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/billing/plans"), newPlan)
response := &CreateSubscriptionPlanResponse{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// UpdateSubscriptionPlan. updates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_patch
// Endpoint: PATCH /v1/billing/plans/:plan_id
func (c *Client) UpdateSubscriptionPlan(ctx context.Context, updatedPlan SubscriptionPlan) error {
req, err := c.NewRequest(ctx, http.MethodPatch, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/billing/plans/", updatedPlan.ID), updatedPlan.GetUpdatePatch())
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// UpdateSubscriptionPlan. updates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_get
// Endpoint: GET /v1/billing/plans/:plan_id
func (c *Client) GetSubscriptionPlan(ctx context.Context, planId string) (*SubscriptionPlan, error) {
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/billing/plans/", planId), nil)
response := &SubscriptionPlan{}
if err != nil {
return response, err
}
err = c.SendWithAuth(req, response)
return response, err
}
// List all plans
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_list
// Endpoint: GET /v1/billing/plans
func (c *Client) ListSubscriptionPlans(ctx context.Context, params *SubscriptionPlanListParameters) (*ListSubscriptionPlansResponse, error) {
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s", c.APIBase, "/v1/billing/plans"), nil)
response := &ListSubscriptionPlansResponse{}
if err != nil {
return response, err
}
if params != nil {
q := req.URL.Query()
q.Add("page", params.Page)
q.Add("page_size", params.PageSize)
q.Add("total_required", params.TotalRequired)
q.Add("product_id", params.ProductId)
q.Add("plan_ids", params.PlanIds)
req.URL.RawQuery = q.Encode()
}
err = c.SendWithAuth(req, response)
return response, err
}
// Activates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_activate
// Endpoint: POST /v1/billing/plans/{id}/activate
func (c *Client) ActivateSubscriptionPlan(ctx context.Context, planId string) error {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/plans/%s/activate", c.APIBase, planId), nil)
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Deactivates a plan
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_deactivate
// Endpoint: POST /v1/billing/plans/{id}/deactivate
func (c *Client) DeactivateSubscriptionPlans(ctx context.Context, planId string) error {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/plans/%s/deactivate", c.APIBase, planId), nil)
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// Updates pricing for a plan. For example, you can update a regular billing cycle from $5 per month to $7 per month.
// Doc: https://developer.paypal.com/docs/api/subscriptions/v1/#plans_update-pricing-schemes
// Endpoint: POST /v1/billing/plans/{id}/update-pricing-schemes
func (c *Client) UpdateSubscriptionPlanPricing(ctx context.Context, planId string, pricingSchemes []PricingSchemeUpdate) error {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s/v1/billing/plans/%s/update-pricing-schemes", c.APIBase, planId), PricingSchemeUpdateRequest{
Schemes: pricingSchemes,
})
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}

96
transaction_search.go Normal file
View File

@ -0,0 +1,96 @@
package paypal
import (
"context"
"fmt"
"strconv"
"time"
)
type TransactionSearchRequest struct {
TransactionID *string
TransactionType *string
TransactionStatus *string
TransactionAmount *string
TransactionCurrency *string
StartDate time.Time
EndDate time.Time
PaymentInstrumentType *string
StoreID *string
TerminalID *string
Fields *string
BalanceAffectingRecordsOnly *string
PageSize *int
Page *int
}
type TransactionSearchResponse struct {
TransactionDetails []SearchTransactionDetails `json:"transaction_details"`
AccountNumber string `json:"account_number"`
StartDate JSONTime `json:"start_date"`
EndDate JSONTime `json:"end_date"`
LastRefreshDatetime JSONTime `json:"last_refreshed_datetime"`
Page int `json:"page"`
SharedListResponse
}
// ListTransactions - Use this to search PayPal transactions from the last 31 days.
// Endpoint: GET /v1/reporting/transactions
func (c *Client) ListTransactions(ctx context.Context, req *TransactionSearchRequest) (*TransactionSearchResponse, error) {
response := &TransactionSearchResponse{}
r, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s%s", c.APIBase, "/v1/reporting/transactions"), nil)
if err != nil {
return nil, err
}
q := r.URL.Query()
q.Add("start_date", req.StartDate.Format(time.RFC3339))
q.Add("end_date", req.EndDate.Format(time.RFC3339))
if req.TransactionID != nil {
q.Add("transaction_id", *req.TransactionID)
}
if req.TransactionType != nil {
q.Add("transaction_type", *req.TransactionType)
}
if req.TransactionStatus != nil {
q.Add("transaction_status", *req.TransactionStatus)
}
if req.TransactionAmount != nil {
q.Add("transaction_amount", *req.TransactionAmount)
}
if req.TransactionCurrency != nil {
q.Add("transaction_currency", *req.TransactionCurrency)
}
if req.PaymentInstrumentType != nil {
q.Add("payment_instrument_type", *req.PaymentInstrumentType)
}
if req.StoreID != nil {
q.Add("store_id", *req.StoreID)
}
if req.TerminalID != nil {
q.Add("terminal_id", *req.TerminalID)
}
if req.Fields != nil {
q.Add("fields", *req.Fields)
}
if req.BalanceAffectingRecordsOnly != nil {
q.Add("balance_affecting_records_only", *req.BalanceAffectingRecordsOnly)
}
if req.PageSize != nil {
q.Add("page_size", strconv.Itoa(*req.PageSize))
}
if req.Page != nil {
q.Add("page", strconv.Itoa(*req.Page))
}
r.URL.RawQuery = q.Encode()
if err = c.SendWithAuth(r, response); err != nil {
return nil, err
}
return response, nil
}

1174
types.go

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,10 @@
package paypalsdk
package paypal
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
type webprofileTestServer struct {
t *testing.T
}
func TestNewClient(t *testing.T) {
c, err := NewClient("", "", "")
if err == nil {
@ -57,8 +50,6 @@ func TestTypeUserInfo(t *testing.T) {
func TestTypeItem(t *testing.T) {
response := `{
"name":"Item",
"price":"22.99",
"currency":"GBP",
"quantity":"1"
}`
@ -69,8 +60,6 @@ func TestTypeItem(t *testing.T) {
}
if i.Name != "Item" ||
i.Price != "22.99" ||
i.Currency != "GBP" ||
i.Quantity != "1" {
t.Errorf("Item decoded result is incorrect, Given: %v", i)
}
@ -144,6 +133,35 @@ func TestTypeErrorResponseTwo(t *testing.T) {
}
}
func TestTypeErrorResponseThree(t *testing.T) {
response := `{
"name": "BUSINESS_ERROR",
"debug_id": "[REDACTED]",
"message": "Business error",
"information_link": "https://developer.paypal.com/webapps/developer/docs/api/#BUSINESS_ERROR",
"details": [
{
"name": "TOKEN_NOT_FOUND",
"message": "Not Found: Invalid BA-Token Identifier"
}
]
}`
i := &ErrorResponse{}
err := json.Unmarshal([]byte(response), i)
if err != nil {
t.Errorf("ErrorResponse Unmarshal failed")
}
if i.Name != "BUSINESS_ERROR" ||
i.Message != "Business error" ||
len(i.Details) != 1 ||
i.Details[0].Name != "TOKEN_NOT_FOUND" ||
i.Details[0].Message != "Not Found: Invalid BA-Token Identifier" {
t.Errorf("ErrorResponse decoded result is incorrect, Given: %v", i)
}
}
func TestTypePayoutResponse(t *testing.T) {
response := `{
"batch_header":{
@ -228,6 +246,134 @@ func TestTypePayoutResponse(t *testing.T) {
}
}
func TestOrderUnmarshal(t *testing.T) {
response := `{
"id": "5O190127TN364715T",
"status": "CREATED",
"links": [
{
"href": "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T",
"rel": "self",
"method": "GET"
},
{
"href": "https://api.sandbox.paypal.com/checkoutnow?token=5O190127TN364715T",
"rel": "approve",
"method": "GET"
},
{
"href": "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T/capture",
"rel": "capture",
"method": "POST"
}
]
}`
order := &Order{}
err := json.Unmarshal([]byte(response), order)
if err != nil {
t.Errorf("Order Unmarshal failed")
}
if order.ID != "5O190127TN364715T" ||
order.Status != "CREATED" ||
order.Links[0].Href != "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T" {
t.Errorf("Order decoded result is incorrect, Given: %+v", order)
}
}
func TestOrderCompletedUnmarshal(t *testing.T) {
response := `{
"id": "1K412082HD5737736",
"status": "COMPLETED",
"purchase_units": [
{
"reference_id": "default",
"amount": {
"currency_code": "EUR",
"value": "99.99"
},
"payee": {
"email_address": "payee@business.example.com",
"merchant_id": "7DVPP5Q2RZJQY"
},
"custom_id": "123456",
"soft_descriptor": "PAYPAL *TEST STORE",
"shipping": {
"name": {
"full_name": "John Doe"
},
"address": {
"address_line_1": "Address, Country",
"admin_area_2": "Area2",
"admin_area_1": "Area1",
"postal_code": "123456",
"country_code": "US"
}
},
"payments": {
"captures": [
{
"id": "6V864560EH247264J",
"status": "COMPLETED",
"amount": {
"currency_code": "EUR",
"value": "99.99"
},
"final_capture": true,
"custom_id": "123456",
"create_time": "2021-07-27T09:39:17Z",
"update_time": "2021-07-27T09:39:17Z"
}
]
}
}
],
"payer": {
"name": {
"given_name": "John",
"surname": "Doe"
},
"email_address": "payer@personal.example.com",
"payer_id": "7D36CJQ2TUEUU",
"address": {
"address_line_1": "City, Country",
"admin_area_2": "Area2",
"admin_area_1": "Area1",
"postal_code": "123456",
"country_code": "US"
}
},
"create_time": "2021-07-27T09:38:37Z",
"update_time": "2021-07-27T09:39:17Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/1K412082HD5737736",
"rel": "self",
"method": "GET"
}
]
}`
order := &Order{}
err := json.Unmarshal([]byte(response), order)
if err != nil {
t.Errorf("Order Unmarshal failed")
}
if order.ID != "1K412082HD5737736" ||
order.Status != "COMPLETED" ||
order.PurchaseUnits[0].Payee.EmailAddress != "payee@business.example.com" ||
order.PurchaseUnits[0].CustomID != "123456" ||
order.PurchaseUnits[0].Shipping.Name.FullName != "John Doe" ||
order.PurchaseUnits[0].Shipping.Address.AdminArea1 != "Area1" ||
order.Payer.Name.GivenName != "John" ||
order.Payer.Address.AddressLine1 != "City, Country" ||
order.Links[0].Href != "https://api.sandbox.paypal.com/v2/checkout/orders/1K412082HD5737736" {
t.Errorf("Order decoded result is incorrect, Given: %+v", order)
}
}
func TestTypePayoutItemResponse(t *testing.T) {
response := `{
"payout_item_id":"9T35G83YA546X",
@ -325,389 +471,3 @@ func TestTypePaymentPatchMarshal(t *testing.T) {
t.Errorf("PaymentPatch response2 is incorrect,\n Given: %+v\n Expected: %+v", string(response2), string(p2expectedresponse))
}
}
// ServeHTTP implements http.Handler
func (ts *webprofileTestServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ts.t.Log(r.RequestURI)
if r.RequestURI == "/v1/payment-experience/web-profiles" {
if r.Method == "POST" {
ts.create(w, r)
}
if r.Method == "GET" {
ts.list(w, r)
}
}
if r.RequestURI == "/v1/payment-experience/web-profiles/XP-CP6S-W9DY-96H8-MVN2" {
if r.Method == "GET" {
ts.getvalid(w, r)
}
if r.Method == "PUT" {
ts.updatevalid(w, r)
}
if r.Method == "DELETE" {
ts.deletevalid(w, r)
}
}
if r.RequestURI == "/v1/payment-experience/web-profiles/foobar" {
if r.Method == "GET" {
ts.getinvalid(w, r)
}
if r.Method == "PUT" {
ts.updateinvalid(w, r)
}
if r.Method == "DELETE" {
ts.deleteinvalid(w, r)
}
}
}
func (ts *webprofileTestServer) create(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
var raw map[string]string
w.Header().Set("Content-Type", "application/json")
if name, ok := data["name"]; !ok || name == "" {
raw = map[string]string{
"name": "VALIDATION_ERROR",
"message": "should have name",
}
w.WriteHeader(http.StatusBadRequest)
} else {
raw = map[string]string{
"id": "XP-CP6S-W9DY-96H8-MVN2",
}
w.WriteHeader(http.StatusCreated)
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) updatevalid(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if ID, ok := data["id"]; !ok || ID != "XP-CP6S-W9DY-96H8-MVN2" {
raw := map[string]string{
"name": "INVALID_RESOURCE_ID",
"message": "id invalid",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
res, _ := json.Marshal(raw)
w.Write(res)
return
}
if name, ok := data["name"]; !ok || name == "" {
raw := map[string]string{
"name": "VALIDATION_ERROR",
"message": "should have name",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
res, _ := json.Marshal(raw)
w.Write(res)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (ts *webprofileTestServer) updateinvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
raw := map[string]interface{}{
"name": "INVALID_RESOURCE_ID",
"message": "foobar not found",
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) getvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
raw := map[string]interface{}{
"id": "XP-CP6S-W9DY-96H8-MVN2",
"name": "YeowZa! T-Shirt Shop",
"presentation": map[string]interface{}{
"brand_name": "YeowZa! Paypal",
"logo_image": "http://www.yeowza.com",
"locale_code": "US",
},
"input_fields": map[string]interface{}{
"allow_note": true,
"no_shipping": 0,
"address_override": 1,
},
"flow_config": map[string]interface{}{
"landing_page_type": "Billing",
"bank_txn_pending_url": "http://www.yeowza.com",
},
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) getinvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
raw := map[string]interface{}{
"name": "INVALID_RESOURCE_ID",
"message": "foobar not found",
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) list(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
raw := []interface{}{
map[string]interface{}{
"id": "XP-CP6S-W9DY-96H8-MVN2",
"name": "YeowZa! T-Shirt Shop",
},
map[string]interface{}{
"id": "XP-96H8-MVN2-CP6S-W9DY",
"name": "Shop T-Shirt YeowZa! ",
},
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) deleteinvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
raw := map[string]interface{}{
"name": "INVALID_RESOURCE_ID",
"message": "foobar not found",
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) deletevalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
}
func TestCreateWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
Name: "YeowZa! T-Shirt Shop",
Presentation: Presentation{
BrandName: "YeowZa! Paypal",
LogoImage: "http://www.yeowza.com",
LocaleCode: "US",
},
InputFields: InputFields{
AllowNote: true,
NoShipping: NoShippingDisplay,
AddressOverride: AddrOverrideFromCall,
},
FlowConfig: FlowConfig{
LandingPageType: LandingPageTypeBilling,
BankTXNPendingURL: "http://www.yeowza.com",
},
}
res, err := c.CreateWebProfile(wp)
if err != nil {
t.Fatal(err)
}
if res.ID != "XP-CP6S-W9DY-96H8-MVN2" {
t.Fatalf("expecting response to have ID = `XP-CP6S-W9DY-96H8-MVN2` got `%s`", res.ID)
}
}
func TestCreateWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{}
_, err := c.CreateWebProfile(wp)
if err == nil {
t.Fatalf("expecting an error got nil")
}
}
func TestGetWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
res, err := c.GetWebProfile("XP-CP6S-W9DY-96H8-MVN2")
if err != nil {
t.Fatal(err)
}
if res.ID != "XP-CP6S-W9DY-96H8-MVN2" {
t.Fatalf("expecting res.ID to have value = `XP-CP6S-W9DY-96H8-MVN2` but got `%s`", res.ID)
}
if res.Name != "YeowZa! T-Shirt Shop" {
t.Fatalf("expecting res.Name to have value = `YeowZa! T-Shirt Shop` but got `%s`", res.Name)
}
}
func TestGetWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
_, err := c.GetWebProfile("foobar")
if err == nil {
t.Fatalf("expecting an error got nil")
}
}
func TestGetWebProfiles(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
res, err := c.GetWebProfiles()
if err != nil {
t.Fatal(err)
}
if len(res) != 2 {
t.Fatalf("expecting two results got %d", len(res))
}
}
func TestSetWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
ID: "XP-CP6S-W9DY-96H8-MVN2",
Name: "Shop T-Shirt YeowZa!",
}
err := c.SetWebProfile(wp)
if err != nil {
t.Fatal(err)
}
}
func TestSetWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
ID: "foobar",
}
err := c.SetWebProfile(wp)
if err == nil {
t.Fatal(err)
}
wp = WebProfile{}
err = c.SetWebProfile(wp)
if err == nil {
t.Fatal(err)
}
}
func TestDeleteWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
ID: "XP-CP6S-W9DY-96H8-MVN2",
Name: "Shop T-Shirt YeowZa!",
}
err := c.SetWebProfile(wp)
if err != nil {
t.Fatal(err)
}
}
func TestDeleteWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
err := c.DeleteWebProfile("foobar")
if err == nil {
t.Fatal(err)
}
}

View File

@ -1,30 +1,31 @@
package paypalsdk
package paypal
import (
"context"
"fmt"
)
// StoreCreditCard func
// Endpoint: POST /v1/vault/credit-cards
func (c *Client) StoreCreditCard(cc CreditCard) (*CreditCard, error) {
req, err := c.NewRequest("POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/vault/credit-cards"), cc)
func (c *Client) StoreCreditCard(ctx context.Context, cc CreditCard) (*CreditCard, error) {
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/vault/credit-cards"), cc)
if err != nil {
return nil, err
}
response := CreditCard{}
response := &CreditCard{}
if err = c.SendWithAuth(req, &response); err != nil {
if err = c.SendWithAuth(req, response); err != nil {
return nil, err
}
return &response, nil
return response, nil
}
// DeleteCreditCard func
// Endpoint: DELETE /v1/vault/credit-cards/credit_card_id
func (c *Client) DeleteCreditCard(id string) error {
req, err := c.NewRequest("DELETE", fmt.Sprintf("%s/v1/vault/credit-cards/%s", c.APIBase, id), nil)
func (c *Client) DeleteCreditCard(ctx context.Context, id string) error {
req, err := c.NewRequest(ctx, "DELETE", fmt.Sprintf("%s/v1/vault/credit-cards/%s", c.APIBase, id), nil)
if err != nil {
return err
}
@ -38,24 +39,24 @@ func (c *Client) DeleteCreditCard(id string) error {
// GetCreditCard func
// Endpoint: GET /v1/vault/credit-cards/credit_card_id
func (c *Client) GetCreditCard(id string) (*CreditCard, error) {
req, err := c.NewRequest("GET", fmt.Sprintf("%s/v1/vault/credit-cards/%s", c.APIBase, id), nil)
func (c *Client) GetCreditCard(ctx context.Context, id string) (*CreditCard, error) {
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s/v1/vault/credit-cards/%s", c.APIBase, id), nil)
if err != nil {
return nil, err
}
response := CreditCard{}
response := &CreditCard{}
if err = c.SendWithAuth(req, &response); err != nil {
if err = c.SendWithAuth(req, response); err != nil {
return nil, err
}
return &response, nil
return response, nil
}
// GetCreditCards func
// Endpoint: GET /v1/vault/credit-cards
func (c *Client) GetCreditCards(ccf *CreditCardsFilter) (*CreditCards, error) {
func (c *Client) GetCreditCards(ctx context.Context, ccf *CreditCardsFilter) (*CreditCards, error) {
page := 1
if ccf != nil && ccf.Page > 0 {
page = ccf.Page
@ -65,33 +66,33 @@ func (c *Client) GetCreditCards(ccf *CreditCardsFilter) (*CreditCards, error) {
pageSize = ccf.PageSize
}
req, err := c.NewRequest("GET", fmt.Sprintf("%s/v1/vault/credit-cards?page=%d&page_size=%d", c.APIBase, page, pageSize), nil)
req, err := c.NewRequest(ctx, "GET", fmt.Sprintf("%s/v1/vault/credit-cards?page=%d&page_size=%d", c.APIBase, page, pageSize), nil)
if err != nil {
return nil, err
}
response := CreditCards{}
response := &CreditCards{}
if err = c.SendWithAuth(req, &response); err != nil {
if err = c.SendWithAuth(req, response); err != nil {
return nil, err
}
return &response, nil
return response, nil
}
// PatchCreditCard func
// Endpoint: PATCH /v1/vault/credit-cards/credit_card_id
func (c *Client) PatchCreditCard(id string, ccf []CreditCardField) (*CreditCard, error) {
req, err := c.NewRequest("PATCH", fmt.Sprintf("%s/v1/vault/credit-cards/%s", c.APIBase, id), ccf)
func (c *Client) PatchCreditCard(ctx context.Context, id string, ccf []CreditCardField) (*CreditCard, error) {
req, err := c.NewRequest(ctx, "PATCH", fmt.Sprintf("%s/v1/vault/credit-cards/%s", c.APIBase, id), ccf)
if err != nil {
return nil, err
}
response := CreditCard{}
response := &CreditCard{}
if err = c.SendWithAuth(req, &response); err != nil {
if err = c.SendWithAuth(req, response); err != nil {
return nil, err
}
return &response, nil
return response, nil
}

144
webhooks.go Normal file
View File

@ -0,0 +1,144 @@
package paypal
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
// CreateWebhook - Subscribes your webhook listener to events.
// Endpoint: POST /v1/notifications/webhooks
func (c *Client) CreateWebhook(ctx context.Context, createWebhookRequest *CreateWebhookRequest) (*Webhook, error) {
req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/notifications/webhooks"), createWebhookRequest)
webhook := &Webhook{}
if err != nil {
return webhook, err
}
err = c.SendWithAuth(req, webhook)
return webhook, err
}
// GetWebhook - Shows details for a webhook, by ID.
// Endpoint: GET /v1/notifications/webhooks/ID
func (c *Client) GetWebhook(ctx context.Context, webhookID string) (*Webhook, error) {
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s%s", c.APIBase, "/v1/notifications/webhooks/", webhookID), nil)
webhook := &Webhook{}
if err != nil {
return webhook, err
}
err = c.SendWithAuth(req, webhook)
return webhook, err
}
// UpdateWebhook - Updates a webhook to replace webhook fields with new values.
// Endpoint: PATCH /v1/notifications/webhooks/ID
func (c *Client) UpdateWebhook(ctx context.Context, webhookID string, fields []WebhookField) (*Webhook, error) {
req, err := c.NewRequest(ctx, http.MethodPatch, fmt.Sprintf("%s/v1/notifications/webhooks/%s", c.APIBase, webhookID), fields)
webhook := &Webhook{}
if err != nil {
return webhook, err
}
err = c.SendWithAuth(req, webhook)
return webhook, err
}
// ListWebhooks - Lists webhooks for an app.
// Endpoint: GET /v1/notifications/webhooks
func (c *Client) ListWebhooks(ctx context.Context, anchorType string) (*ListWebhookResponse, error) {
if len(anchorType) == 0 {
anchorType = AncorTypeApplication
}
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s", c.APIBase, "/v1/notifications/webhooks"), nil)
q := req.URL.Query()
q.Add("anchor_type", anchorType)
req.URL.RawQuery = q.Encode()
resp := &ListWebhookResponse{}
if err != nil {
return nil, err
}
err = c.SendWithAuth(req, resp)
return resp, err
}
// DeleteWebhook - Deletes a webhook, by ID.
// Endpoint: DELETE /v1/notifications/webhooks/ID
func (c *Client) DeleteWebhook(ctx context.Context, webhookID string) error {
req, err := c.NewRequest(ctx, http.MethodDelete, fmt.Sprintf("%s/v1/notifications/webhooks/%s", c.APIBase, webhookID), nil)
if err != nil {
return err
}
err = c.SendWithAuth(req, nil)
return err
}
// VerifyWebhookSignature - Use this to verify the signature of a webhook recieved from paypal.
// Endpoint: POST /v1/notifications/verify-webhook-signature
func (c *Client) VerifyWebhookSignature(ctx context.Context, httpReq *http.Request, webhookID string) (*VerifyWebhookResponse, error) {
type verifyWebhookSignatureRequest struct {
AuthAlgo string `json:"auth_algo,omitempty"`
CertURL string `json:"cert_url,omitempty"`
TransmissionID string `json:"transmission_id,omitempty"`
TransmissionSig string `json:"transmission_sig,omitempty"`
TransmissionTime string `json:"transmission_time,omitempty"`
WebhookID string `json:"webhook_id,omitempty"`
Event json.RawMessage `json:"webhook_event,omitempty"`
}
// Read the content
var bodyBytes []byte
if httpReq.Body != nil {
bodyBytes, _ = io.ReadAll(httpReq.Body)
} else {
return nil, errors.New("cannot verify webhook for HTTP Request with empty body")
}
// Restore the io.ReadCloser to its original state
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
verifyRequest := verifyWebhookSignatureRequest{
AuthAlgo: httpReq.Header.Get("PAYPAL-AUTH-ALGO"),
CertURL: httpReq.Header.Get("PAYPAL-CERT-URL"),
TransmissionID: httpReq.Header.Get("PAYPAL-TRANSMISSION-ID"),
TransmissionSig: httpReq.Header.Get("PAYPAL-TRANSMISSION-SIG"),
TransmissionTime: httpReq.Header.Get("PAYPAL-TRANSMISSION-TIME"),
WebhookID: webhookID,
Event: json.RawMessage(bodyBytes),
}
response := &VerifyWebhookResponse{}
req, err := c.NewRequest(ctx, "POST", fmt.Sprintf("%s%s", c.APIBase, "/v1/notifications/verify-webhook-signature"), verifyRequest)
if err != nil {
return nil, err
}
if err = c.SendWithAuth(req, response); err != nil {
return nil, err
}
return response, nil
}
// GetWebhookEventTypes - Lists all webhook event types.
// Endpoint: GET /v1/notifications/webhooks-event-types
func (c *Client) GetWebhookEventTypes(ctx context.Context) (*WebhookEventTypesResponse, error) {
req, err := c.NewRequest(ctx, http.MethodGet, fmt.Sprintf("%s%s", c.APIBase, "/v1/notifications/webhooks-event-types"), nil)
q := req.URL.Query()
req.URL.RawQuery = q.Encode()
resp := &WebhookEventTypesResponse{}
if err != nil {
return nil, err
}
err = c.SendWithAuth(req, resp)
return resp, err
}

View File

@ -1,6 +1,7 @@
package paypalsdk
package paypal
import (
"context"
"fmt"
"net/http"
)
@ -10,15 +11,15 @@ import (
// Allows for the customisation of the payment experience
//
// Endpoint: POST /v1/payment-experience/web-profiles
func (c *Client) CreateWebProfile(wp WebProfile) (*WebProfile, error) {
func (c *Client) CreateWebProfile(ctx context.Context, wp WebProfile) (*WebProfile, error) {
url := fmt.Sprintf("%s%s", c.APIBase, "/v1/payment-experience/web-profiles")
req, err := c.NewRequest("POST", url, wp)
if err != nil {
return &WebProfile{}, err
}
req, err := c.NewRequest(ctx, "POST", url, wp)
response := &WebProfile{}
if err != nil {
return response, err
}
if err = c.SendWithAuth(req, response); err != nil {
return response, err
}
@ -29,11 +30,11 @@ func (c *Client) CreateWebProfile(wp WebProfile) (*WebProfile, error) {
// GetWebProfile gets an exists payment experience from Paypal
//
// Endpoint: GET /v1/payment-experience/web-profiles/<profile-id>
func (c *Client) GetWebProfile(profileID string) (*WebProfile, error) {
func (c *Client) GetWebProfile(ctx context.Context, profileID string) (*WebProfile, error) {
var wp WebProfile
url := fmt.Sprintf("%s%s%s", c.APIBase, "/v1/payment-experience/web-profiles/", profileID)
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return &wp, err
@ -44,20 +45,20 @@ func (c *Client) GetWebProfile(profileID string) (*WebProfile, error) {
}
if wp.ID == "" {
return &wp, fmt.Errorf("paypalsdk: unable to get web profile with ID = %s", profileID)
return &wp, fmt.Errorf("paypal: unable to get web profile with ID = %s", profileID)
}
return &wp, nil
}
// GetWebProfiles retreieves web experience profiles from Paypal
// GetWebProfiles retrieves web experience profiles from Paypal
//
// Endpoint: GET /v1/payment-experience/web-profiles
func (c *Client) GetWebProfiles() ([]WebProfile, error) {
func (c *Client) GetWebProfiles(ctx context.Context) ([]WebProfile, error) {
var wps []WebProfile
url := fmt.Sprintf("%s%s", c.APIBase, "/v1/payment-experience/web-profiles")
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return wps, err
@ -73,15 +74,15 @@ func (c *Client) GetWebProfiles() ([]WebProfile, error) {
// SetWebProfile sets a web experience profile in Paypal with given id
//
// Endpoint: PUT /v1/payment-experience/web-profiles
func (c *Client) SetWebProfile(wp WebProfile) error {
func (c *Client) SetWebProfile(ctx context.Context, wp WebProfile) error {
if wp.ID == "" {
return fmt.Errorf("paypalsdk: no ID specified for WebProfile")
return fmt.Errorf("paypal: no ID specified for WebProfile")
}
url := fmt.Sprintf("%s%s%s", c.APIBase, "/v1/payment-experience/web-profiles/", wp.ID)
req, err := c.NewRequest("PUT", url, wp)
req, err := c.NewRequest(ctx, "PUT", url, wp)
if err != nil {
return err
@ -97,11 +98,11 @@ func (c *Client) SetWebProfile(wp WebProfile) error {
// DeleteWebProfile deletes a web experience profile from Paypal with given id
//
// Endpoint: DELETE /v1/payment-experience/web-profiles
func (c *Client) DeleteWebProfile(profileID string) error {
func (c *Client) DeleteWebProfile(ctx context.Context, profileID string) error {
url := fmt.Sprintf("%s%s%s", c.APIBase, "/v1/payment-experience/web-profiles/", profileID)
req, err := c.NewRequest("DELETE", url, nil)
req, err := c.NewRequest(ctx, "DELETE", url, nil)
if err != nil {
return err