Skip to content

Commit

Permalink
feat: ideal topups (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
stickyPiston authored Jan 2, 2025
2 parents 4dae09f + e84ce3c commit 31b321c
Show file tree
Hide file tree
Showing 18 changed files with 721 additions and 355 deletions.
46 changes: 37 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ Copy `sample.env` to `.env` and make sure the database options are correct. By d

```bash
docker compose up -d
uv run --env-file .env ./manage.py migrate
```

In development, create an admin superuser

```bash
uv run --env-file .env ./manage.py createsuperuser
uv run --env-file .env manage.py migrate
```

Then depending on whether you want to use a local version of koala, you need to do some additional setup:
Expand Down Expand Up @@ -78,12 +72,46 @@ Then depending on whether you want to use a local version of koala, you need to
Copy the application id and secret into the `.env` file and make sure you update the oauth urls to point to koala.dev.svsticky.nl.
Then complete the `.env` file by filling out the following values:
```env
USER_URL=https://koala.dev.svsticky.nl
ALLOWED_HOSTS=localhost
OIDC_RP_CLIENT_ID=<secret from koala>
OIDC_RP_CLIENT_SECRET=<secret from koala>
OIDC_OP_AUTHORIZATION_ENDPOINT=https://koala.dev.svsticky.nl/api/oauth/authorize
OIDC_OP_TOKEN_ENDPOINT=https://koala.dev.svsticky.nl/api/oauth/token
OIDC_OP_USER_ENDPOINT=https://koala.dev.svsticky.nl/oauth/userinfo
OIDC_OP_JWKS_ENDPOINT=https://koala.dev.svsticky.nl/oauth/discovery/keys
OIDC_OP_LOGOUT_ENDPOINT=https://koala.dev.svsticky.nl/signout
```
### iDeal payments
If you want to work with the iDeal payment system, make sure you have the mollie api key. If you leave it blank, mongoose will still work, except for submitting the top up form. For development you want to use a test token, which can be found in the IT Crowd bitwarden.
```env
MOLLIE_API_KEY=test_<secret from bitwarden>
```
To do test payments, you need to use [ngrok](https://ngrok.com/) to forward your local mongoose installation to a public domain, so that mollie can send webhook requests to your local installation. If you have mongoose running as usual, then you only need to run the following command in a separate terminal:
```bash
ngrok http http://localhost:8000
```
ngrok will open a tunnel and bind your mongoose to a public url, update the `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` fields to include the url from ngrok. Lastly, update the koala oauth application (at `<koala_url>/api/oauth/applications` as explained above) to use the ngrok url as an additional callback uri.
Visiting the ngrok url should give your mongoose installation, and you can just use that url to continue development.
## Running
``` bash
# Database
# Start the database, if it wasn't already running
docker compose up -d

# Server
uv run --env-file .env ./manage.py runserver
uv run --env-file .env manage.py runserver
```
5 changes: 5 additions & 0 deletions admin_board_view/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django import forms


class TopUpForm(forms.Form):
amount = forms.DecimalField(min_value=0, decimal_places=2, required=True)
21 changes: 13 additions & 8 deletions admin_board_view/static/AdminBoardView/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const addCategory = document.getElementById("add-category");
if (addCategory) {
addCategory.addEventListener("click", e => {
const table = document.getElementById("categories");
let template = document.getElementById("template-category-item").cloneNode(deep=true);
let template = document.getElementById("template-category-item").cloneNode(deep = true);
template.classList = "category-item";
template.id = 0;
table.appendChild(template)
Expand All @@ -15,7 +15,7 @@ const updateCategories = document.getElementById("update-categories");
if (updateCategories) {
updateCategories.addEventListener("click", e => {
let items = [];

const categoryItems = Array.from(document.getElementsByClassName("category-item"));
categoryItems.forEach(category => {
let name = category.querySelector(".form-control").value.trim();
Expand Down Expand Up @@ -60,7 +60,7 @@ function delete_category(id) {
url: `/category/edit`,
data: {
"csrfmiddlewaretoken": csrf_token,
"categories": JSON.stringify([{"id": id, "delete": true}])
"categories": JSON.stringify([{ "id": id, "delete": true }])
},
type: "post"
}).then(response => {
Expand All @@ -74,7 +74,7 @@ const addVAT = document.getElementById("add-vat");
if (addVAT) {
addVAT.addEventListener("click", e => {
const table = document.getElementById("vat");
let template = document.getElementById("template-vat-item").cloneNode(deep=true);
let template = document.getElementById("template-vat-item").cloneNode(deep = true);
template.classList = "vat-item";
template.id = 0;
table.appendChild(template)
Expand All @@ -86,7 +86,7 @@ const updateVAT = document.getElementById("update-vat");
if (updateVAT) {
updateVAT.addEventListener("click", e => {
let items = [];

const vatItems = Array.from(document.getElementsByClassName("vat-item"));
vatItems.forEach(vat => {
let percentage = vat.querySelector(".form-control").value.trim();
Expand Down Expand Up @@ -130,7 +130,7 @@ function delete_vat(id) {
url: `/vat/edit`,
data: {
"csrfmiddlewaretoken": csrf_token,
"vat": JSON.stringify([{"id": id, "delete": true}])
"vat": JSON.stringify([{ "id": id, "delete": true }])
},
type: "post"
}).then(response => {
Expand Down Expand Up @@ -166,9 +166,14 @@ if (exportTransactions) {
exportTransactions.addEventListener("click", e => {
const from = document.getElementById("from-date").value;
const to = document.getElementById("to-date").value;
const export_dropdown = document.getElementById("export-type")

const export_dropdown = document.getElementById("export-type");
const export_type = export_dropdown.options[export_dropdown.selectedIndex].value;
const url = `/transactions/export?type=${export_type}&start_date=${from}&end_date=${to}`;

const reponse_dropdown = document.getElementById("response-type");
const response_type = reponse_dropdown.options[reponse_dropdown.selectedIndex].value;

const url = `/transactions/export?type=${export_type}&start_date=${from}&end_date=${to}&response_type=${response_type}`;
window.open(url, "_blank");
});
}
Expand Down
48 changes: 43 additions & 5 deletions admin_board_view/templates/transactions.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ <h5>Export top-up transactions</h5>
<section class="row">
<div class="col">
<label for="from-date" class="form-label">From: </label>
<input type="date" id="from-date" class="form-control" value="{% now "Y-m-d" %}"/>
<input type="date" id="from-date" class="form-control" value="{{ last_week|date:"Y-m-d" }}"/>
</div>
<div class="col">
<label for="to-date" class="form-label">To: </label>
<input type="date" id="to-date" class="form-control" value="{% now "Y-m-d" %}"/>
<input type="date" id="to-date" class="form-control" value="{{ this_week|date:"Y-m-d" }}"/>
</div>
<div class="col">
<label for="export-type" class="form-label">Type of export: </label>
Expand All @@ -23,6 +23,13 @@ <h5>Export top-up transactions</h5>
<option value="mollie">Mollie</option>
</select>
</div>
<div class="col">
<label for="response-type" class="form-label">Export format: </label>
<select id="response-type" class="form-select">
<option value="json">JSON</option>
<option value="csv">csv</option>
</select>
</div>
</section>
<button id="export-top-ups" class="btn btn-primary mt-2">Export</button>
<hr/>
Expand All @@ -32,15 +39,19 @@ <h5>Transaction history</h5>
<button class="nav-link" id="sales-tab" data-bs-toggle="tab" data-bs-target="#sales" type="button" role="tab">Sales</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="top-ups-tab" data-bs-toggle="tab" data-bs-target="#top-ups" type="button" role="tab">Top Ups</button>
<button class="nav-link" id="top-ups-tab" data-bs-toggle="tab" data-bs-target="#top-ups" type="button" role="tab">PIN top ups</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="ideal-tab" data-bs-toggle="tab" data-bs-target="#ideal" type="button" role="tab">iDeal top ups</button>
</li>
</ul>
<div class="tab-content overflow-x-auto" id="myTabContent">
<!-- Product sales -->
<div class="tab-pane fade" id="sales" role="tabpanel">
<table class="table table-striped table-hover text-center align-middle">
<thead>
<tr>
<th>UserID</th>
<th>User</th>
<th>Date</th>
<th>Transaction sum</th>
<th>Products</th>
Expand All @@ -63,11 +74,13 @@ <h5>Transaction history</h5>
</table>
{% include "pagination_footer.html" with page=sales page_name='sales' %}
</div>

<!-- Pin payments -->
<div class="tab-pane fade" id="top-ups" role="tabpanel">
<table class="table table-striped table-hover text-center align-middle">
<thead>
<tr>
<th>UserID</th>
<th>User</th>
<th>Date</th>
<th>Sum</th>
<th>Type</th>
Expand All @@ -86,6 +99,31 @@ <h5>Transaction history</h5>
</table>
{% include "pagination_footer.html" with page=top_ups page_name='top_ups' %}
</div>

<!-- iDeal payments -->
<div class="tab-pane fade" id="ideal" role="tabpanel">
<table class="table table-striped table-hover text-center align-middle">
<thead>
<tr>
<th>User</th>
<th>Date</th>
<th>Sum</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for top_up in ideal.object_list %}
<tr>
<td><a href="/users/{{ top_up.user_id.id }}">{{ top_up.user_id.name }}</a></td>
<td>{{ top_up.date }}</td>
<td>€{{ top_up.transaction_sum }}</td>
<td>iDeal</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "pagination_footer.html" with page=top_ups page_name='top_ups' %}
</div>
</div>
</div>
</div>
Expand Down
8 changes: 4 additions & 4 deletions admin_board_view/templates/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ <h5 class="card-header">Top ups</h5>
</tr>
</thead>
<tbody>
{% for top_up in top_ups.object_list %}
{% for date, sum, type in top_ups %}
<tr>
<td>{{ top_up.date }}</td>
<td>€{{ top_up.transaction_sum }}</td>
<td>{% if top_up.type == 1 %}Pin{% elif top_up.type == 2 %}Credit card{% elif top_up.type == 3 %}Mollie{% endif %}</td>
<td>{{ date }}</td>
<td>€{{ sum }}</td>
<td>{{ type }}</td>
</tr>
{% endfor %}
</tbody>
Expand Down
40 changes: 36 additions & 4 deletions admin_board_view/templates/user_home.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
{% load static %}
{% block body %}
<main class="container mt-3">
{% if transaction %}
{% if transaction.status == PaymentStatus.PAID %}
<div class="alert alert-success">
The transaction was successful and your balance is updated!
</div>
{% elif transaction.status == PaymentStatus.CANCELLED %}
<div class="alert alert-danger">
The transaction failed! If you believe this is a mistake, please contact the board.
</div>
{% else %}
<div class="alert alert-info">
We are processing your payment. Once it succeed, your balance will be updated!
</div>
{% endif %}
{% endif %}
<h1>Welcome {{ user_info.name }}</h1>
<h5 class="mb-2">
Current balance: <b>{{ user_info.euro_balance }}</b>
Expand Down Expand Up @@ -51,16 +66,33 @@ <h5 class="card-header">Top ups</h5>
</tr>
</thead>
<tbody>
{% for top_up in top_ups.object_list %}
{% for date, price, type in top_ups %}
<tr>
<td>{{ top_up.date }}</td>
<td>€{{ top_up.transaction_sum }}</td>
<td>{% if top_up.type == 1 %}Pin{% elif top_up.type == 2 %}Credit card{% elif top_up.type == 3 %}Mollie{% endif %}</td>
<td>{{ date }}</td>
<td>€{{ price }}</td>
<td>{{ type }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "pagination_footer.html" with page=top_ups page_name='top_ups' %}
<hr />
<form action="/api/topup" method="post">
<h5>Top up balance</h5>
{% csrf_token %}
<div class="form-row row align-items-center">
<div class="col-sm-3">
<label for="amount" class="sr-only">Amount: </label>
</div>
<div class="form-group col">
<input placeholder="Amount" class="form-control" type="number" min="0" step="0.01" name="amount" />
</div>
<div class="form-group col">
<button type="submit" class="btn btn-primary">Top up</button>
</div>
</div>
<small id="feeHelp" class="form-text text-muted">Plus &euro;{{TRANSACTION_FEE}} transaction fee.</small>
</form>
</div>
</div>
</div>
Expand Down
13 changes: 7 additions & 6 deletions admin_board_view/utils.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
from django.core.paginator import Paginator
from django.db.models.query import QuerySet


def create_paginator(data, page_n, p_len=5):
"""
Create paginator for data.
Args:
data: Data to paginate
page: Page number
p_len: Length of the page, defaults to 5
"""
if isinstance(data, QuerySet):
if hasattr(data.model, 'date'):
if hasattr(data.model, "date"):
# Order data, most recent date first
data = data.order_by('date', 'id').reverse()
elif hasattr(data.model, 'name'):
data = data.order_by('name', 'id')
data = data.order_by("date").reverse()
elif hasattr(data.model, "name"):
data = data.order_by("name", "id")
else:
data = data.order_by('id')
data = data.order_by("id")

page = None
paginator = Paginator(data, p_len)
Expand Down
Loading

0 comments on commit 31b321c

Please sign in to comment.