Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Merge pull request #3909 from gratipay/subscriptions-api
Browse files Browse the repository at this point in the history
Subscriptions api
  • Loading branch information
chadwhitacre committed Feb 9, 2016
2 parents 3bd1049 + 63c74dd commit ca9c608
Show file tree
Hide file tree
Showing 3 changed files with 339 additions and 0 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,47 @@ an object giving a point-in-time snapshot of Gratipay. The
- `undefined` (key not present)—no OpenStreetMap account connected
- `http://www.openstreetmap.org/user/%openstreetmap_username`
**/`%username`/payment-instructions.json**
([source](https://github.com/gratipay/gratipay.com/www/~/%username/payment-instructions.json.spt))—*private*—Responds
to `GET` with an array of objects representing your current payment
instructions. A payment instruction is created when a ~user instructs Gratipay
to make voluntary payments to a Team. Pass a `team_slug` with `GET` to fetch
payment instruction only for that particular team. `POST` an array of objects
containing `team_slug` and `amount` to bulk upsert payment instructions (make
sure to set `Content-Type` to `application/json`). The `amount` must be encoded
as a string rather than a number. In case the upsert is not successful for any
object, there will be an `error` attribute in the response explaining the error
along with the `team_slug` to identify the object for which the error occured.
This endpoint requires authentication. Look up your user ID and API key on your
[account page](https://gratipay.com/about/me/settings/) and pass them using
basic auth.
E.g.:
Request
```
curl -L https://gratipay.com/username/payment-instructions.json \
-u $userid:$api_key \
-X POST \
-d '[{"amount": "1.00", "team_slug": "foobar"}]' \
-H "Content-Type: application/json"
```
Response
```
[
{
"amount": "1.00",
"ctime": "2016-01-30T12:38:00.182230+00:00",
"due": "0.00",
"mtime": "2016-02-06T14:37:28.532508+00:00",
"team_name": "Foobar team",
"team_slug": "foobar"
}
]
```
API Implementations
-------------------
Expand Down
199 changes: 199 additions & 0 deletions tests/py/test_payment_instructions_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import json

from gratipay.testing import Harness

class TestPaymentInstructionApi(Harness):

def test_get_with_team_filter(self):
"Test that GET with team_slug passed returns correct response."

alice = self.make_participant("alice", claimed_time='now')
Enterprise = self.make_team("The Enterprise", is_approved=True)
Trident = self.make_team("The Trident", is_approved=True)

alice.set_payment_instruction(Enterprise, '10.0')
alice.set_payment_instruction(Trident, '12.0')

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json?team_slug=" + Enterprise.slug
, auth_as='alice').body)

assert data['team_slug'] == Enterprise.slug
assert data['team_name'] == Enterprise.name
assert data['amount'] == '10.00'
assert 'ctime' in data
assert 'mtime' in data
assert 'due' in data

def test_get_with_team_filter_raises_error_on_invalid_team_slug(self):
self.make_participant("alice", claimed_time = 'now')

response = self.client.GxT(
"~/alice/payment-instructions.json?team_slug=no-team"
, auth_as='alice')

assert response.code == 400

# pi => payment instruction.

def test_get_with_team_filter_returns_default_if_no_pi(self):
self.make_participant("alice", claimed_time='now')
Enterprise = self.make_team("The Enterprise", is_approved=True)

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json?team_slug=" + Enterprise.slug
, auth_as='alice').body)

assert data['team_slug'] == Enterprise.slug
assert data['team_name'] == Enterprise.name
assert data['amount'] == '0.00'
assert data['due'] == '0.00'
assert data['ctime'] == None
assert data['mtime'] == None

def test_simple_get(self):
"Test that GET without any parameters returns correct response."

alice = self.make_participant("alice", claimed_time='now')
Enterprise = self.make_team("The Enterprise", is_approved=True)
Trident = self.make_team("The Trident", is_approved=True)

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)

assert len(data) == 0 # Empty 'array' should be returned.

alice.set_payment_instruction(Enterprise, '10.0')
alice.set_payment_instruction(Trident, '12.0')

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)

assert len(data) == 2

assert data[0]['team_slug'] == Trident.slug # response is ordered by amount desc
assert data[0]['amount'] == '12.00'
assert data[0]['team_name'] == Trident.name

assert data[1]['team_slug'] == Enterprise.slug
assert data[1]['amount'] == '10.00'
assert data[1]['team_name'] == Enterprise.name

for d in data:
assert 'due' in d
assert 'ctime' in d
assert 'mtime' in d

def test_post(self):
"Test that POST to this endpoint works correctly."

self.make_participant("alice", claimed_time='now')
Enterprise = self.make_team("The Enterprise", is_approved=True)
Trident = self.make_team("The Trident", is_approved=True)

request_body = [
{ 'amount': "1.50", 'team_slug': Enterprise.slug },
{ 'amount': "39.50", 'team_slug': Trident.slug }
]

response = self.client.POST( "~/alice/payment-instructions.json"
, body=json.dumps(request_body)
, content_type='application/json'
, auth_as='alice')

assert response.code == 200

# Make sure apt response returned.
data = json.loads(response.body)

assert len(data) == 2

assert data[0]['team_slug'] == Enterprise.slug
assert data[0]['team_name'] == Enterprise.name
assert data[0]['amount'] == '1.50'

assert data[1]['team_slug'] == Trident.slug
assert data[1]['team_name'] == Trident.name
assert data[1]['amount'] == '39.50'

for d in data:
assert 'due' in d
assert 'ctime' in d
assert 'mtime' in d

# Make sure actually written to database.
data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)

assert data[0]['team_slug'] == Trident.slug
assert data[0]['amount'] == '39.50'

assert data[1]['team_slug'] == Enterprise.slug
assert data[1]['amount'] == '1.50'

def test_post_with_no_team_slug_key_returns_error(self):
self.make_participant("alice", claimed_time='now')

response = self.client.POST( "~/alice/payment-instructions.json"
, body=json.dumps([{ 'amount': "1.50" }])
, content_type='application/json'
, auth_as='alice')

assert response.code == 200 # Since batch processing.
assert 'error' in json.loads(response.body)[0]

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)
assert len(data) == 0

def test_post_with_no_amount_key_returns_error(self):
self.make_participant("alice", claimed_time='now')
Enterprise = self.make_team("The Enterprise", is_approved=True)

response = self.client.POST( "~/alice/payment-instructions.json"
, body=json.dumps(
[{ 'team_slug': Enterprise.slug }])
, content_type='application/json'
, auth_as='alice')

assert response.code == 200
assert 'error' in json.loads(response.body)[0]

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)
assert len(data) == 0

def test_adding_pi_for_invalid_team_returns_error(self):
self.make_participant("alice", claimed_time='now')

request_body = [{ 'team_slug': 'no-slug', 'amount': '39.50' }]

response = self.client.POST( "~/alice/payment-instructions.json"
, body=json.dumps(request_body)
, content_type='application/json'
, auth_as='alice')

assert response.code == 200
assert 'error' in json.loads(response.body)[0]

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)
assert len(data) == 0

def test_adding_pi_for_unapproved_team_returns_error(self):
self.make_participant("alice", claimed_time='now')
Enterprise = self.make_team("The Enterprise", is_approved=False)

request_body = [{ 'team_slug': Enterprise.slug, 'amount': '39.50' }]

response = self.client.POST( "~/alice/payment-instructions.json"
, body=json.dumps(request_body)
, content_type='application/json'
, auth_as='alice')

assert response.code == 200
assert 'error' in json.loads(response.body)[0]

data = json.loads(self.client.GET(
"~/alice/payment-instructions.json", auth_as='alice').body)
assert len(data) == 0
99 changes: 99 additions & 0 deletions www/~/%username/payment-instructions.json.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
""" Get or change authenticated user's payment instructions.
"""
from aspen import Response
from gratipay.models.team import Team
from gratipay.utils import get_participant

[-----------------------------------------------------------------------------]

def format_payment_instruction(p):
return {
"team_name": p.team_name,
"team_slug": p.team_slug,
"amount": str(p.amount),
"due": str(p.due),
"ctime": p.ctime,
"mtime": p.mtime
}

def format_error(team_slug, error):
return {
"team_slug": team_slug,
"error": error
}

participant = get_participant(state, restrict=True)

if request.method == 'GET':

slug = request.qs.get("team_slug", "")

if slug:

# Make sure queried team exists.
team = Team.from_slug(slug)
if not team:
raise Response(400, _("Invalid team slug."))

pi, totals = participant.get_giving_for_profile()

out = {}
for p in pi:
if p.team_slug == slug:
out = format_payment_instruction(p)
break
if not out:
# Payment instruction for this team not found.
# Return default.
out = {
"team_name": team.name,
"team_slug": team.slug,
"amount": "0.00",
"due": "0.00",
"ctime": None,
"mtime": None
}
else:
pi, totals = participant.get_giving_for_profile()
out = [format_payment_instruction(p) for p in pi]

elif request.method == 'POST':
out = []
new_payment_instructions = request.body

for pi in new_payment_instructions:
if 'team_slug' not in pi:
one = format_error(None, "No team slug.")
elif 'amount' not in pi:
one = format_error(pi['team_slug'], "No amount.")
else:
team = Team.from_slug(pi['team_slug'])
if team and team.is_approved and not team.is_closed:
try:
created_pi = participant.set_payment_instruction(
team, parse_decimal(pi['amount'])
)
except Exception, exc:
one = format_error(team.slug, exc.__class__.__name__)
else:
# Payment instruction successfully created.
# Create response.
one = {
"team_name": team.name,
"team_slug": team.slug,
"ctime": created_pi['ctime'],
"mtime": created_pi['mtime'],
"amount": str(created_pi['amount']),
"due": str(created_pi['due'])
}
else:
one = format_error(pi['team_slug'],"Invalid or inactive team.")

out.append(one)

else:
# Only allow GET, POST.
raise Response(405, "", {"Allow": "GET, POST"})

[---] application/json
out

0 comments on commit ca9c608

Please sign in to comment.