Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add opencollective payment POST /pay endpoint #397

Merged
merged 17 commits into from
May 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
jobsika
postgres-data
.env
.opencollective-env

#node
node_modules
Expand Down
3 changes: 3 additions & 0 deletions backend/.opencollective-env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OPEN_COLLECTIVE_API_URL=https://api.opencollective.com/graphql/v2
OPEN_COLLECTIVE_API_KEY=<API_KEY>
OPEN_COLLECTIVE_ORG_SLUG=<ORG_SLUG>
8 changes: 4 additions & 4 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ build:
## run: run the api
.PHONY: run
run:
go run .
ENVIRONMENT=development go run .

## run4test: run the api in test environment (fixes timeouts)
.PHONY: run4test
run4test:
ENVIRONMENT=test go run .
ENVIRONMENT=development TEST=true go run .

## e2etest: run end to end tests against local api
## : run the api using `ENVIRONMENT=dev make run` or `make run4test` to avoid timeouts
## : run the api using `ENVIRONMENT=development TEST=true make run` or `make run4test` to avoid timeouts
.PHONY: e2etest
e2etest: $(E2ETEST_DEPS)
-rm -rf postgres-data; cd ./e2etests/ && API_HOST="http://localhost:7000/" npm test
-cd ./e2etests/ && API_HOST="http://localhost:7000/" npm test
@make reset-postgres > /dev/null

## e2etest-compose: run end to end tests in the docker-compose.test.yaml
Expand Down
72 changes: 72 additions & 0 deletions backend/doc/payment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Open collective payment

The payment method we use for jobsika is based off of the opencollective [GraphQL API](https://graphql-docs-v2.opencollective.com/welcome).
We are using opencollective because the money that we receive lands directly on our opencollective account.
Which makes it easier to access that money and spend it in the open.

### How to develop ?

You are a developer and you would like to test or modify our payment solution. You are at the right the place,
this document aims to provide you enough information to get you started.

#### Prerequisites

In order to contribute to the payment solution you will need to have an account to opencollective, and have admin
access to an organisation.

You can request admin access to [openbilling](https://opencollective.com/openbilling), [osscameroon](https://opencollective.com/osscameroon) or create your own organisation.


Once you have admin access to an organisation, you need to generate an API Key. To do so, go to your personal settings on opencollective select the `For developers` section and create a new personal token.
Make sure you to set the `Account` and `Webhooks` scopes for that personal token.

**For developers tab**

![For developers](./res/opencollective-for-developers.png)

**Create a new token**

![Create a new token](./res/create-a-new-token.png)


**Copy your personal token**

![Copy your personal token](./res/copy-your-personal-token.png)


#### Setup

Once you have your personal token, clone the [jobsika](https://github.com/osscameroon/jobsika) repository and head to the `./backend` folder.
Copy the `.opencollective-env-example` and rename it to `.opencollective-env`. Then replace the `OPENCOLLECTIVE_API_KEY` with your personal token.
and the `OPENCOLLECTIVE_ORG_SLUG` with the slug of the organisation you have admin access to.

Now you should be able to run the backend and make payment requests.

Run the api using `make start-api`

Then run the command `curl -vsS -X POST -H 'Content-Type: application/json' -d '{"email":"[email protected]", "tier": "je suis con", "job_offer_id": "1"}' http://localhost:7000/pay | jq`

You should have a similar output to:

```
{
"tier_url": "https://opencollective.com/osscameroon/contribute/jobsika-joboffer-56908"
}
```

The `tier_url` is the link to the newly created payment tier.


Before you can proceed to the next step you must setup a `Webhook`. Go to the organisation profile and select the `Webhooks` tab.
Then click on `Add a new webhook` and fill the form as shown bellow.

You will need to expose your local api to the internet. You can use [ngrok](https://ngrok.com/) to do so.

Once you have generated a public url, you can set the URL field to `https://<whatever-your-ngrok-domain-is>/open-collective-webhook`.
The `/open-collective-webhook` is the endpoint that the api exposes to receive webhooks from opencollective.

**Webhook form**
[![Webhook form](./res/webhook-form.png)](./res/webhook-form.png)


Now you can go back to the `tier_url` your received from the previous payment request and click on the `Pay now` button.
Binary file added backend/doc/res/copy-your-personal-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backend/doc/res/create-a-new-token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backend/doc/res/webhook-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion backend/docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ services:
env_file:
- ./.docker-env
environment:
- ENVIRONMENT=test
- TEST=true
- ENVIRONMENT=development
volumes:
- .:/api
ports:
Expand Down
46 changes: 46 additions & 0 deletions backend/e2etests/pay.test.after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect } from "chai";
import request from "supertest";
import dotenv from "dotenv";

dotenv.config();
const apiHost = process.env.API_HOST;
const endpoint = "pay";

describe(`${endpoint}`, function () {
describe("POST", function () {
it("fails to make a payment with invalid email address", async function () {
return request(apiHost)
.post(`${endpoint}`)
.set("Accept", "application/json")
.send({
email: "wrong email",
tier: "new jobsika tier",
job_offer_id: "1",
})
.expect(400)
.expect("Content-Type", "application/json; charset=utf-8")
.then((res) => {
expect(JSON.stringify(res.body)).contain("email field is invalid");
});
});

it("proceed with a payment", async function () {
return request(apiHost)
.post(`${endpoint}`)
.set("Accept", "application/json")
.send({
email: "[email protected]",
tier: "new jobsika tier",
job_offer_id: "1",
})
.expect(200)
.expect("Content-Type", "application/json; charset=utf-8")
.then((res) => {
expect(JSON.stringify(res.body)).contain(
"opencollective.com"
);
});
});

});
});
3 changes: 3 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.4 // indirect
github.com/machinebox/graphql v0.2.2
github.com/matryer/is v1.4.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/text v0.3.7
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
Expand Down
13 changes: 12 additions & 1 deletion backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"os"

"github.com/joho/godotenv"
"github.com/osscameroon/jobsika/internal/payment"
"github.com/osscameroon/jobsika/internal/storage"
)

var (
postgresEnvFile = ".postgres-env"
postgresEnvFile = ".postgres-env"
openCollectiveEnvFile = ".opencollective-env"
//StageEnv stage environment
StageEnv = "stage"
//ProdEnv production environment
Expand All @@ -23,13 +25,17 @@ type Config struct {
//Environment can be set to stage or production
Environment string
JWTKey string

//OCOpts contains the open collective options
OCOpts payment.OpenCollectiveOptions
}

// GetDefaultConfig returns a config with default values and env variables
func GetDefaultConfig() Config {
if defaultConfig == nil {
//load postgres env variables
godotenv.Load(postgresEnvFile)
godotenv.Load(openCollectiveEnvFile)

defaultConfig = &Config{
DBOpts: storage.DBOptions{
Expand All @@ -41,6 +47,11 @@ func GetDefaultConfig() Config {
},
Environment: os.Getenv("ENVIRONMENT"),
JWTKey: os.Getenv("JWT_KEY"),
OCOpts: payment.OpenCollectiveOptions{
URL: os.Getenv("OPEN_COLLECTIVE_API_URL"),
KEY: os.Getenv("OPEN_COLLECTIVE_API_KEY"),
OrgSlug: os.Getenv("OPEN_COLLECTIVE_ORG_SLUG"),
},
}
}

Expand Down
50 changes: 50 additions & 0 deletions backend/internal/graphql/graphql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package graphql

import (
"context"

"github.com/machinebox/graphql"
)

// IClient is the interface for the client
type IClient interface {
Run(req *graphql.Request, variables map[string]interface{}, response interface{}) error
}

// Client is the client
type Client struct {
c *graphql.Client
key string
}

// Run query data from the graphql client
func (c *Client) Run(req *graphql.Request, variables map[string]interface{}, response interface{}) error {
req.Header.Set("Api-Key", c.key)

//set variables
for k, v := range variables {
req.Var(k, v)
}

ctx := context.Background()
if err := c.c.Run(ctx, req, response); err != nil {
return err
}

return nil
}

// Query converts a string into a graphql.Request
func Query(q string) *graphql.Request {
return graphql.NewRequest(q)
}

// NewClient creates a new client
func NewClient(url, key string) *Client {
client := &Client{
c: graphql.NewClient(url),
key: key,
}

return client
}
Loading