Skip to content
This repository was archived by the owner on Apr 5, 2024. It is now read-only.

Commit e6d164c

Browse files
committed
Merge branch 'develop'
2 parents b563eee + 833b274 commit e6d164c

28 files changed

+740
-284
lines changed

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ RUN pip install --no-cache-dir -r deploy/requirements.txt
1515

1616
COPY . .
1717

18-
RUN npm install -g npm && ./build-resources && apt-get remove -y npm && apt autoremove -y
18+
# Upgrading NPM will cause an error "Unexpected token =" during the build
19+
# RUN npm install -g npm && ./build-resources && apt-get remove -y npm && apt autoremove -y
1920

2021
RUN mkdir -p www/media
2122

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[![Build Status](https://api.travis-ci.org/City-of-Helsinki/respa.svg?branch=master)](https://travis-ci.org/City-of-Helsinki/respa)
22
[![codecov](https://codecov.io/gh/City-of-Helsinki/respa/branch/master/graph/badge.svg)](https://codecov.io/gh/City-of-Helsinki/respa)
33
[![Requirements Status](https://requires.io/github/City-of-Helsinki/respa/requirements.svg?branch=master)](https://requires.io/github/City-of-Helsinki/respa/requirements/?branch=master)
4+
[![Build Status](https://dev.azure.com/City-of-Helsinki/respa/_apis/build/status/City-of-Helsinki.respa?repoName=City-of-Helsinki%2Frespa&branchName=develop)](https://dev.azure.com/City-of-Helsinki/respa/_build/latest?definitionId=106&repoName=City-of-Helsinki%2Frespa&branchName=develop)
45

56
Respa – Resource reservation and management service
67
===================

azure-pipelines-develop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
trigger:
66
branches:
77
include:
8-
- frozendevelop
8+
- develop
99
paths:
1010
exclude:
1111
- README.md

docs/payments.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ There are a couple of required configuration keys that need to be set in order t
1313
- `RESPA_PAYMENTS_ENABLED`: Whether payments are enabled or not. Boolean `True`/`False`. The default value is `False`.
1414
- `RESPA_PAYMENTS_PROVIDER_CLASS`: Dotted path to the active provider class e.g. `payments.providers.BamboraPayformProvider` as a string. No default value.
1515
- `RESPA_PAYMENTS_PAYMENT_WAITING_TIME`: In minutes, how old the potential unpaid orders/reservations have to be in order for Respa cleanup to set them expired. The default value is `15`.
16+
- `RESPA_PAYMENTS_PAYMENT_REQUESTED_WAITING_TIME`: In hours, how old requested unpaid orders/reservations have to be after staff confirmation in order for Respa cleanup to set them expired. The default value is `24`.
17+
1618

1719
`./manage.py expire_too_old_unpaid_orders` runs the order/reservation cleanup for current orders. You'll probably want to run it periodically at least in production. [Cron](https://en.wikipedia.org/wiki/Cron) is one candidate for doing that.
1820

@@ -26,20 +28,22 @@ In addition to the general configuration keys mentioned in the previous section,
2628
- `RESPA_PAYMENTS_BAMBORA_API_KEY`: Identifies which merchant store account to use with Bambora. Value can be found in the merchant portal. Provided as a string. No default value.
2729
- `RESPA_PAYMENTS_BAMBORA_API_SECRET`: Used to calculate hashes out of the data being sent and received, to verify it is not being tampered with. Also found in the merchant portal and provided as a string. No default value.
2830
- `RESPA_PAYMENTS_BAMBORA_PAYMENT_METHODS`: An array of payment methods to show to the user to select from e.g.`['nordea', 'creditcards']`. Full list of supported values can be found in [the currencies section of](https://payform.bambora.com/docs/web_payments/?page=full-api-reference#currencies) Bambora's API documentation page.
31+
- `RESPA_PAYMENTS_BAMBORA_TOKEN_VALID_DAYS`: In days, how long payment token used for payment link is valid and usable for customer to make payment.
32+
2933

3034
## Basics
3135

3236
Model `Product` represents everything that can be ordered and paid alongside a reservation. Products are linked to one or multiple resources.
3337

3438
There are currently two types of products:
3539

36-
- `rent`: At least one product of type `rent` must be ordered when such is available on the resource.
40+
- `rent`: At least one product of type `rent` must be ordered when such is available on the resource.
3741

3842
- `extra`: Ordering of products of type `extra` is not mandatory, so when there are only `extra` products available, one can create a reservation without an order. However, when an order is created, even with just extra product(s), it must be paid to get the reservation confirmed.
3943

4044
Everytime a product is saved, a new copy of it is created in the db, so product modifying does not affect already existing orders.
4145

42-
All prices are in euros. A product's price is stored in `price` field. However, there are different ways the value should be interpreted depending on `price_type` field's value:
46+
All prices are in euros. A product's price is stored in `price` field. However, there are different ways the value should be interpreted depending on `price_type` field's value:
4347

4448
- `fixed`: The price stays always the same regardless of the reservation, so if `price` is `10.00` the final price is 10.00 EUR.
4549

@@ -49,7 +53,11 @@ Model `Order` represents orders of products. One and only one order is linked to
4953

5054
An order can be in state `waiting`, `confirmed`, `rejected`, `expired` or `cancelled`. A new order will start from state `waiting`, and from there it will change to one of the other states. Only valid other state change is from `confirmed` to `cancelled`.
5155

52-
An order is created by providing its data in `order` field when creating a reservation via the API. The UI must also provide a return URL to which the user will be redirected after the payment process has been completed. In the creation response the UI gets back a payment URL, to which it must redirect the user to start the actual payment process.
56+
An order is created by providing its data in `order` field when creating a reservation via the API. The UI must also provide a return URL to which the user will be redirected after the payment process has been completed. In the creation response the UI gets back a payment URL, to which it must redirect the user to start the actual payment process.
57+
58+
## Requested reservations
59+
60+
Payment integration has been implemented to support reservations in resources that requires staff confirmation. In that case payment link is sent to reserver in email after staff has changed reservations state to `waiting_for_payment`. `NotificationTemplate` with `NotificationType` `RESERVATION_WAITING_FOR_PAYMENT` must be defined and the template body must include `{{payment_url}}` tag for payment link to be included in the email. Default wait time for payment before order gets expired is 24 hours and it can be changed with setting `RESPA_PAYMENTS_PAYMENT_REQUESTED_WAITING_TIME`.
5361

5462
## Administration
5563

@@ -235,6 +243,23 @@ Example full return url: `https://varaamo.hel.fi/payment-return-url/?payment_sta
235243

236244
Modifying an order is not possible, and after a reservation's creation the `order` field is read-only.
237245

246+
### Custom price
247+
248+
Admins and some staff members with permission `can_set_custom_price_for_reservations` can set custom price for individual reservation. The custom price is set with PUT-request to Reservations details endpoint.
249+
250+
Example of custom_price field in Reservation PUT request
251+
252+
```json
253+
...
254+
255+
"custom_price": {
256+
"price": "10.00",
257+
"price_type": "half"
258+
}
259+
260+
...
261+
```
262+
238263
### Order data in reservation API endpoint
239264

240265
Reservation data in the API includes `order` field when the current user has permission to view it (either own reservation or via the explicit view order permission).

docs/permissions.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ can_create_reservations_for_other_users X X X
140140
can_create_overlapping_reservations X X X X
141141
can_ignore_max_reservations_per_user X X X X
142142
can_ignore_max_period X X X X
143+
can_set_custom_price_for_reservations X X X X
143144
====================================== ====== ======= ====== ====== ======
144145

145146

@@ -208,6 +209,9 @@ can_ignore_max_reservations_per_user
208209
can_ignore_max_period
209210
Can ignore resources max period rule
210211

212+
can_set_custom_price_for_reservations
213+
Can set custom price for individual reservations
214+
211215

212216
Respa Admin Permissions
213217
~~~~~~~~~~~~~~~~~~~~~~~
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.13 on 2020-09-28 13:00
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('notifications', '0005_add_access_code_created_notification'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='notificationtemplate',
15+
name='type',
16+
field=models.CharField(choices=[('reservation_requested', 'Reservation requested'), ('reservation_requested_official', 'Reservation requested official'), ('reservation_cancelled', 'Reservation cancelled'), ('reservation_confirmed', 'Reservation confirmed'), ('reservation_created', 'Reservation created'), ('reservation_denied', 'Reservation denied'), ('reservation_created_with_access_code', 'Reservation created with access code'), ('reservation_access_code_created', 'Access code was created for a reservation'), ('reservation_waiting_for_payment', 'Reservation waiting for payment'), ('catering_order_created', 'Catering order created'), ('catering_order_modified', 'Catering order modified'), ('catering_order_deleted', 'Catering order deleted'), ('reservation_comment_created', 'Reservation comment created'), ('catering_order_comment_created', 'Catering order comment created')], db_index=True, max_length=100, unique=True, verbose_name='Type'),
17+
),
18+
]

notifications/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class NotificationType:
3232
# we don't confuse the user with "new reservation created"-style
3333
# messaging.
3434
RESERVATION_ACCESS_CODE_CREATED = 'reservation_access_code_created'
35+
RESERVATION_WAITING_FOR_PAYMENT = 'reservation_waiting_for_payment'
3536
CATERING_ORDER_CREATED = 'catering_order_created'
3637
CATERING_ORDER_MODIFIED = 'catering_order_modified'
3738
CATERING_ORDER_DELETED = 'catering_order_deleted'
@@ -54,6 +55,7 @@ class NotificationTemplate(TranslatableModel):
5455
(NotificationType.RESERVATION_DENIED, _('Reservation denied')),
5556
(NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE, _('Reservation created with access code')),
5657
(NotificationType.RESERVATION_ACCESS_CODE_CREATED, _('Access code was created for a reservation')),
58+
(NotificationType.RESERVATION_WAITING_FOR_PAYMENT, _('Reservation waiting for payment')),
5759

5860
(NotificationType.CATERING_ORDER_CREATED, _('Catering order created')),
5961
(NotificationType.CATERING_ORDER_MODIFIED, _('Catering order modified')),

payments/api/reservation.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
)
88
from resources.api.reservation import ReservationSerializer
99

10-
from ..models import OrderLine, Product
10+
from ..models import OrderLine, Product, ReservationCustomPrice
1111
from ..providers import get_payment_provider
1212
from .base import OrderSerializerBase
1313

@@ -18,7 +18,7 @@ class ReservationEndpointOrderSerializer(OrderSerializerBase):
1818
payment_url = serializers.SerializerMethodField()
1919

2020
class Meta(OrderSerializerBase.Meta):
21-
fields = OrderSerializerBase.Meta.fields + ('id', 'return_url', 'payment_url')
21+
fields = OrderSerializerBase.Meta.fields + ('id', 'return_url', 'payment_url', 'is_requested_order')
2222

2323
def create(self, validated_data):
2424
order_lines_data = validated_data.pop('order_lines', [])
@@ -32,6 +32,8 @@ def create(self, validated_data):
3232
ui_return_url=return_url)
3333
try:
3434
self.context['payment_url'] = payments.initiate_payment(order)
35+
order.payment_url = self.context['payment_url']
36+
order.save()
3537
except DuplicateOrderError as doe:
3638
raise exceptions.APIException(detail=str(doe),
3739
code=status.HTTP_409_CONFLICT)
@@ -81,9 +83,14 @@ def to_representation(self, instance):
8183

8284
return data
8385

86+
class ReservationEndpointCustomPriceSerializer(serializers.ModelSerializer):
87+
class Meta:
88+
model = ReservationCustomPrice
89+
fields = ('price', 'price_type')
8490

8591
class PaymentsReservationSerializer(ReservationSerializer):
8692
order = serializers.SlugRelatedField('order_number', read_only=True)
93+
custom_price = ReservationEndpointCustomPriceSerializer(required=False)
8794

8895
def __init__(self, *args, **kwargs):
8996
super().__init__(*args, **kwargs)
@@ -104,7 +111,7 @@ def __init__(self, *args, **kwargs):
104111
self.fields['order'] = ReservationEndpointOrderSerializer(read_only=True)
105112

106113
class Meta(ReservationSerializer.Meta):
107-
fields = ReservationSerializer.Meta.fields + ['order']
114+
fields = ReservationSerializer.Meta.fields + ['order', 'custom_price']
108115

109116
def to_representation(self, instance):
110117
data = super().to_representation(instance)
@@ -118,18 +125,42 @@ def to_representation(self, instance):
118125
def create(self, validated_data):
119126
order_data = validated_data.pop('order', None)
120127
reservation = super().create(validated_data)
128+
prefetched_user = self.context.get('prefetched_user', None)
129+
user = prefetched_user or self.context['request'].user
121130

122131
if order_data:
123132
if not reservation.can_add_product_order(self.context['request'].user):
124133
raise PermissionDenied()
125134

126135
order_data['reservation'] = reservation
136+
resource = reservation.resource
137+
if resource.need_manual_confirmation and not resource.can_bypass_manual_confirmation(user):
138+
order_data['is_requested_order'] = True
139+
127140
ReservationEndpointOrderSerializer(context=self.context).create(validated_data=order_data)
128141

129142
return reservation
130143

144+
def update(self, instance, validated_data):
145+
custom_price_data = validated_data.pop('custom_price', None)
146+
reservation = super().update(instance, validated_data)
147+
prefetched_user = self.context.get('prefetched_user', None)
148+
user = prefetched_user or self.context['request'].user
149+
150+
if custom_price_data:
151+
if not reservation.can_set_custom_price(user):
152+
raise PermissionDenied()
153+
if hasattr(reservation, 'custom_price'):
154+
reservation.custom_price.delete()
155+
custom_price_data['reservation'] = reservation
156+
ReservationEndpointCustomPriceSerializer(context=self.context).create(validated_data=custom_price_data)
157+
158+
return reservation
159+
131160
def validate(self, data):
132161
order_data = data.pop('order', None)
162+
custom_price_data = data.pop('custom_price', None)
133163
data = super().validate(data)
164+
data['custom_price'] = custom_price_data
134165
data['order'] = order_data
135166
return data
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.13 on 2020-09-28 09:15
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('payments', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='order',
15+
name='payment_url',
16+
field=models.CharField(blank=True, default='', max_length=200, verbose_name='payment url'),
17+
),
18+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 2.2.13 on 2020-09-28 14:54
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('payments', '0002_order_payment_url'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='order',
15+
name='confirmed_by_staff_at',
16+
field=models.DateTimeField(blank=True, null=True, verbose_name='confirmed by staff at'),
17+
),
18+
migrations.AddField(
19+
model_name='order',
20+
name='is_requested_order',
21+
field=models.BooleanField(default=False, verbose_name='is requested order'),
22+
),
23+
]

0 commit comments

Comments
 (0)