Pricing
BillingService
class
The Each time a user creates, changes, cancels or ends a trip, a transaction is added to the billing account of the user for the group the reservation is in.
This transaction contains a bill with the cost (in credits).
The bill is computed by an instance of a BillingService
class.
Which instance to use depends on the configuration of the vehicle service unit (car config).
Depending on the needs, the price model for a group can be implemented as a configuration of one of the built-in billing
services (in which case the possibilities depend on the implementation of the existing service) or it can be implemented
as a new class that extends the BillingService
class (in which case almost anything is possible, but very group
specific things are added to the general repos and any change the price model needs an update).
The BillingService
has the following hooks:
- onReservationCreated is called when a reservation is created. It takes a
ReservationDetails
object of the newly created reservation. - onReservationCanceled is called when a reservation is canceled. It takes a
ReservationDetails
object of the reservation that is canceled. - onReservationUnconsumed is called when a reservation was not started before it's end time. It takes the
ReservationDetails
object of the reservation. - onReservationChanged is called when a reservation is changed. It takes two
ReservationDetails
objects: the old details and the updated details. - onUsageEnded is called when a trip has ended. It takes the
ReservationDetails
object and aTripInfo
object.
All these hooks output a Bill
object, which contains a number of BillItem
s.
A BillItem
consists of a type
(a key for internal use), a description
(for showing to the user), a quantity
(e.g.
number of minutes or number of kms), a price
and info
(key-value map with additional info).
The sum of the prices of all items, is the total amount for the bill and will be deducted from the billing account of the user.
An example json of a BillItem
:
{
"description": "2 minutes added to reservation",
"price": {
"currency": "credits",
"value": 10
},
"quantity": {
"unit": "min",
"value": 2
},
"type": "reservation"
}
The built-in billing service
This billing service allows to create a configurable price model based on pre-defined measures. The configuration of the price model happens based on a json.
Each time one of the hooks is called, first a basket is created consisting of BasketItem
s.
These items describe the consumed quantity
of a particular type
.
For example, the number of kilometers driven or the number of minutes reserved.
A quantity
can be a simple measure (the amount of a particular unit) or a time period (possibly multiple intervals).
BasketItems
The following basket items are created for the different hooks:
- onReservationCreated
- reservation_create 1 piece of reservation cost
- reservation the reserved period
- onReservationCanceled
- canceled_time_refund the reserved period that is to be refunded
- canceled_create_refund 1 piece of reservation cost
- onReservationChanged
- canceled_time_refund the period(s) that were canceled
- reservation the period(s) that were added (excluding the over time periods)
- over_time_use the period the user was already over time
- onUsageEnded
- early_use the period between the effective start time and reserved start time
- over_time_penalty 1 piece of penalty if user is over time
- over_time_use the period the user was over time
- remaining_time_refund the period between effective end time and reserved end time
- distance the number of driven kms
- charged_percentage the percentage of battery charged during the trip
- charged_energy the kWh charged during the trip (calculated based on percentage and battery capacity of the car model)
- discharged_percentage the percentage of battery discharged during the trip
- discharged_energy the energy (kWh) consumed during the trip (calculated based on percentage and battery capacity of the car model)
- trip_duration the effective duration of the reservation
- onReservationUnconsumed
- reservation_unconsumed the unconsumed period
(onReservationChanged
currently does not have the over_time_penalty
but to be consistent it should)
Configuration
The configuration of the price model is a json that looks like (this json is typically stored in the firebase database, but could also come from elsewhere):
{
"items": {
"reservation": {
"description": {
"en": "{product.quantity.value, number, integer} minutes added to reservation",
"nl": "..."
},
"price": "8-21 1 credits/min 21-8 0.5 credits/min"
},
"canceled_time_refund": {
"description": {
"en": "{product.quantity.value, number, integer} minutes removed from reservation"
},
"price": "<now+1day ? 8-21 -0.5 credits/min 21-8 -0.25 credits/min;>now+1day ? 8-21 -1 credits/min 21-8 -0.5 credits/min"
},
"reservation_create": {
"description": {
"en": "reservation fee"
},
"price": "30 credits"
},
"canceled_create_refund": {
"description": {
"en": "refund reservation fee"
},
"price": ">now&&<now+1day ? -15 credits;>now+1day ? -30 credits"
},
"distance": {
"description": {
"en": "{product.quantity.value, number, integer} km driven"
},
"price": "1 credits/km"
},
"discharged_energy": {
"description": {
"en": "usage fee ({product.quantity.value} kWh)"
},
"price": "1.5 credits/0.1 kWh"
}
}
}
The items
is a map with keys that correspond to the basket items and a value that contains a description and a price definition.
The description is a localizable string that can contain interpolations following the ICU syntax.
The price definition is a string that defines the amount of money per unit of the quantity.
For time periods, it may also contain time intervals for which different prices apply. For example, the above price
model will charge 1 credit per minute during the day from 8am till 9pm and 0.5 credits during the night.
You can also define periods relative to the current time for which different prices apply. For example, the above model
will refund 50% of a canceled reservation if within 24 hours and 100% otherwise.
Generic measures
Currently, the price model contains a number of pre-defined measures. It"s possible to define new new custom measures inside the json defining the price model.
For example, the following would define an amount of kilometers included in a reservation based on the duration of the trip.
{
"distance_over_limit": {
"measure": "max(0, tripInfo.distance - (reservation.duration > 360 ? 80 : reservation.duration > 180 ? 60 : 40))",
"description": {
"en": "{product.quantity.value, number, integer} extra kms driven"
},
"price": "1 credits/km"
}
}
Generic measures are defined following the syntax of the expressions package.
Grace period
Surcharges and discounts
Templates
An external REST billing service
Instead of using a built in billing service, it"s also possible to use an external service over http. This service would receive a json with all the information about the transaction to be charged and return a json with the bill information.
The REST billing service (RestBillingService
class) has the same hooks and there respective BasketItems
as local billing service. See above
- onReservationCreated
- onReservationCanceled
- onReservationChanged
- onUsageEnded
- onReservationUnconsumed
The external REST service is called with a POST request. The request body contains the BasketItem
s corresponding to the hook/action and a set of price model parameters. This is a set of parameters that give context to the billing. The RestBillingService
accepts a response body with one or more BillItem
s.
If a valid Bill
is received from the external REST billing service a transaction will be added to the users" billing account
Example request body
{
"action":"usage-ended",
"priceModelParameters": {
"reservation": {
"group": {
"id": "cvba"
},
},
"vehicleModel": {
"category":
"small_car"
},
"billingAccount": {
"subscription": {
"product": {
"type": "subscription",
"id": "coop_formula"
},
"shop": {
"id": "cvba_2022"
}
},
"creditCurrency": "EUR"
}
},
"items": {
[
{
"type": "remaining_time_refund",
"quantity": {
"unit": "min",
"value": 26
}
},
{
"type": "distance",
"quantity": {
"unit": "km",
"value": 23
}
}
]
}
}
Example repsonse body
{
"items": {
[
{
"description": "26 minuten not used",
"price": {
"currency": "credits",
"value": -104
},
"quantity": {
"unit": "min",
"value": 26
},
"type": "remaining_time_refund"
},
{
"description": "23 km driven",
"price": {
"currency": "credits",
"value": 46
},
"quantity": {
"unit": "km",
"value": 23
},
"type": "distance"
},
{
"description": "special discount for you",
"price": {
"currency": "credits",
"value": -5
},
"quantity": {
"unit": "piece",
"value": 1
},
"type": "discount"
}
]
}
}
The BillItem
s can correspond to the passed BasketItem
s, but this is not obligatory. Every BillItem
corresponds to a line in the transaction details so this can be completely independant from the BasketItem
s.
Setting the price model of a vehicle service unit (car config)
Which billing service/price model to use is defined on the level of a car config in the field priceModel
.
The priceModel
field should contain a uri that uniquely identifies the billing service/price model to be used.
When the protocol of the uri is equal to partago
, the built in billing service is used and the path of the uri is used
to lookup a price model in the firebase database. For example, the uri partago:///priceModels/partago/v2/zoe_22
refers to
a price model defined in firebase at the path priceModels/partago/v2/zoe_22
.
When the protocol of the uri is equal to http
or https
, a billing service that contacts an external REST service
will be used.
When more billing services are implemented and added to the backend, they should be uniquely identifiable through the uri.
The uri can also contain string interpolations. For example, partago:///priceModels/partago/v2/{reservation.group.id}
,
will use a different price model depending on the group used for the reservation.
Currently, the only parameter that can be used here is reservation.group.id
.
This could be extended to allow accessing information like the type of subscription of the user.