Compare commits

...

9 Commits

Author SHA1 Message Date
Alex
9d25fa005c gorm model fixes 2025-05-20 12:41:21 +02:00
Alex
20a693a80c Setting Consents userid null upon deletion 2025-05-14 18:25:53 +02:00
Alex
242d37713d unscoped delete 2025-05-14 15:36:20 +02:00
Alex
4b83be6ed8 templates and constraints 2025-05-14 15:27:10 +02:00
Alex
8fcb73f24d logging 2025-05-14 12:36:46 +02:00
Alex
11740cb503 fixes in prod 2025-05-14 12:29:47 +02:00
Alex
06f8078b17 chg: mail templates 2025-05-14 12:13:12 +02:00
Alex
18f5dadb06 wip 2025-04-10 15:40:22 +02:00
Alex
87f08dd3be subscription_model -> subscription 2025-03-24 18:00:57 +01:00
57 changed files with 1881 additions and 1172 deletions

22
frontend/src/app.d.ts vendored
View File

@@ -17,7 +17,7 @@ interface Membership {
start_date: string | '';
end_date: string | '';
parent_member_id: number | -1;
subscription_model: Subscription;
subscription: Subscription;
}
interface BankAccount {
@@ -51,7 +51,6 @@ interface User {
last_name: string | '';
password: string | '';
phone: string | '';
notes: string | '';
address: string | '';
zip_code: string | '';
city: string | '';
@@ -60,11 +59,9 @@ interface User {
role_id: number | -1;
dateofbirth: string | '';
company: string | '';
profile_picture: string | '';
payment_status: number | -1;
membership: Membership;
bank_account: BankAccount;
licence: Licence;
membership: Membership | null;
bank_account: BankAccount | null;
licence: Licence | null;
notes: string | '';
}
@@ -80,9 +77,9 @@ interface Car {
end_date: string | '';
color: string | '';
licence_plate: string | '';
location: Location;
damages: Damage[] | [];
insurances: Insurance[] | [];
location: Location | null;
damages: Damage[] | null;
insurances: Insurance[] | null;
notes: string | '';
}
@@ -93,8 +90,11 @@ interface Location {
interface Damage {
id: number | -1;
opponent: User;
name: string | '';
opponent: User | null;
driver_id: number | -1;
insurance: Insurance | null;
date: string | '';
notes: string | '';
}

View File

@@ -5,8 +5,10 @@
import { applyAction, enhance } from '$app/forms';
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
import { defaultCar } from '$lib/utils/defaults';
import { defaultDamage, defaultInsurance, defaultOpponent } from '$lib/utils/defaults';
import { PERMISSIONS } from '$lib/utils/constants';
import Modal from './Modal.svelte';
import UserEditForm from './UserEditForm.svelte';
const dispatch = createEventDispatcher();
@@ -16,19 +18,54 @@
/** @type {App.Locals['user'] } */
export let editor;
/** @type {App.Types['car'] | null} */
/** @type {App.Locals['users'] } */
export let users;
/** @type {App.Types['car']} */
export let car;
console.log('Opening car modal with:', car);
$: car = car || { ...defaultCar() };
$: console.log(
'damage.opponent changed:',
car?.damages.map((d) => d.opponent)
);
$: console.log(
'damage.insurance changed:',
car?.damages.map((d) => d.insurance)
);
// TODO: Remove when working
// $: if (car.damages.length > 0 && !car.damages.every((d) => d.insurance && d.opponent)) {
// car.damages = car.damages.map((damage) => ({
// ...damage,
// insurance: damage.insurance ?? defaultInsurance(),
// opponent: damage.opponent ?? defaultOpponent()
// }));
// }
let initialized = false; // Prevents infinite loops
// Ensure damages have default values once `car` is loaded
$: if (car && !initialized) {
car = {
...car,
damages:
car.damages?.map((damage) => ({
...damage,
insurance: damage.insurance ?? defaultInsurance(),
opponent: damage.opponent ?? defaultOpponent()
})) || []
};
initialized = true; // Prevents re-running
}
$: isLoading = car === undefined || editor === undefined;
let isUpdating = false;
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
/** @type {number | null} */
let editingUserIndex = null;
const TABS = ['car.car', 'insurance', 'car.damages'];
let activeTab = TABS[0];
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
/** @type {import('@sveltejs/kit').SubmitFunction} */
const handleUpdate = async () => {
isUpdating = true;
return async ({ result }) => {
@@ -47,7 +84,7 @@
<SmallLoader width={30} message={$t('loading.car_data')} />
{:else if editor && car}
<form class="content" action="?/updateCar" method="POST" use:enhance={handleUpdate}>
<input name="susbscription[id]" type="hidden" bind:value={car.id} />
<input name="car[id]" type="hidden" bind:value={car.id} />
<h1 class="step-title" style="text-align: center;">
{car.id ? $t('car.edit') : $t('car.create')}
</h1>
@@ -155,48 +192,357 @@
/>
</div>
<div class="tab-content" style="display: {activeTab === 'insurance' ? 'block' : 'none'}">
{#each car.insurances as insurance}
<InputField
name="car[insurance][company]"
label={$t('company')}
bind:value={insurance.company}
placeholder={$t('placeholder.company')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][reference]"
label={$t('insurance.reference')}
bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][start_date]"
type="date"
label={$t('start')}
bind:value={insurance.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][end_date]"
type="date"
label={$t('end')}
bind:value={insurance.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][notes]"
type="textarea"
label={$t('notes')}
bind:value={insurance.notes}
placeholder={$t('placeholder.notes', {
values: { name: insurance.company || '' }
})}
rows={10}
/>
{/each}
<div class="accordion">
{#each car.insurances as insurance, index}
<input hidden value={insurance?.id} name="car[insurances][{index}][id]" />
<details class="accordion-item" open={index === car.insurances.length - 1}>
<summary class="accordion-header">
{insurance.company ? insurance.company : ''}
{insurance.reference ? ' (' + insurance.reference + ')' : ''}
</summary>
<div class="accordion-content">
<InputField
name="car[insurances][{index}][company]"
label={$t('company')}
bind:value={insurance.company}
placeholder={$t('placeholder.company')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][reference]"
label={$t('insurance_reference')}
bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][start_date]"
type="date"
label={$t('start')}
bind:value={insurance.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][end_date]"
type="date"
label={$t('end')}
bind:value={insurance.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][notes]"
type="textarea"
label={$t('notes')}
bind:value={insurance.notes}
placeholder={$t('placeholder.notes', {
values: { name: insurance.company || '' }
})}
rows={10}
/>
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
<button
type="button"
class="btn btn-delete danger"
on:click={() => {
if (
confirm(
$t('dialog.insurance_deletion', {
values: {
name: insurance.company + ' (' + insurance.reference + ')'
}
})
)
) {
car.insurances = car.insurances.filter((_, i) => i !== index);
}
}}
>
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
</details>
{/each}
</div>
<div class="button-group">
{#if hasPrivilige(editor, PERMISSIONS.Create)}
<button
type="button"
class="btn primary"
on:click={() => {
car.insurances = [...car.insurances, defaultInsurance()];
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
</div>
<div class="tab-content" style="display: {activeTab === 'car.damages' ? 'block' : 'none'}">
<div class="accordion">
{#each car.damages as damage, index (damage.id)}
<input type="hidden" name="car[damages][{index}][id]" value={damage.id} />
<details class="accordion-item" open={index === car.damages.length - 1}>
<summary class="accordion-header">
<span class="nav-badge">
{damage.name} -
{damage.opponent.first_name}
{damage.opponent.last_name}
</span>
</summary>
<div class="accordion-content">
<InputField
name="car[damages][{index}][date]"
type="date"
label={$t('date')}
bind:value={damage.date}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][name]"
label={$t('car.damages')}
bind:value={damage.name}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][driver_id]"
type="select"
label={$t('user.member')}
options={users
?.filter((u) => u.role_id > 0)
.map((u) => ({
value: u.id,
label: `${u.first_name} ${u.last_name}`,
color: '--subtext1'
})) || []}
bind:value={damage.driver_id}
readonly={readonlyUser}
/>
<h4>{$t('user.opponent')}</h4>
<input
hidden
name={`car[damages][${index}][opponent][id]`}
value={car.damages[index].opponent.id}
/>
<input
hidden
name={`car[damages][${index}][opponent][email]`}
value={car.damages[index].opponent.email}
/>
<input
hidden
name={`car[damages][${index}][opponent][first_name]`}
value={car.damages[index].opponent.first_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][last_name]`}
value={damage.opponent.last_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][phone]`}
value={damage.opponent.phone}
/>
<input
hidden
name={`car[damages][${index}][opponent][address]`}
value={damage.opponent.address}
/>
<input
hidden
name={`car[damages][${index}][opponent][city]`}
value={damage.opponent.city}
/>
<input
hidden
name={`car[damages][${index}][opponent][zip_code]`}
value={damage.opponent.zip_code}
/>
<input
hidden
name={`car[damages][${index}][opponent][notes]`}
value={damage.opponent.notes}
/>
<input
hidden
name={`car[damages][${index}][opponent][role_id]`}
value={damage.opponent.role_id}
/>
<input
hidden
name={`car[damages][${index}][opponent][status]`}
value={damage.opponent.status}
/>
<input
hidden
name={`car[damages][${index}][opponent][dateofbirth]`}
value={damage.opponent.dateofbirth}
/>
<input
hidden
name={`car[damages][${index}][opponent][company]`}
value={damage.opponent.company}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][id]`}
value={damage.opponent.bank_account.id}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][mandate_date_signed]`}
value={damage.opponent.bank_account.mandate_date_signed}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][bank]`}
value={damage.opponent.bank_account.bank}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][account_holder_name]`}
value={damage.opponent.bank_account.account_holder_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][iban]`}
value={damage.opponent.bank_account.iban}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][bic]`}
value={damage.opponent.bank_account.bic}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][mandate_reference]`}
value={damage.opponent.bank_account.mandate_reference}
/>
<details class="accordion-item">
<summary class="accordion-header">
<span class="nav-badge">
{#if damage.opponent?.first_name}
{damage.opponent.first_name} {damage.opponent.last_name}
{:else}
{$t('not_set')}
{/if}
</span>
</summary>
<div class="accordion-content">
<table class="table">
<tbody>
<tr>
<th>{$t('email')}</th>
<td>{damage.opponent?.email || '-'}</td>
</tr>
<tr>
<th>{$t('phone')}</th>
<td>{damage.opponent?.phone || '-'}</td>
</tr>
<tr>
<th>{$t('address')}</th>
<td>{damage.opponent?.address || '-'}</td>
</tr>
<tr>
<th>{$t('city')}</th>
<td>{damage.opponent?.city || '-'}</td>
</tr>
<tr>
<th>{$t('zip_code')}</th>
<td>{damage.opponent?.zip_code || '-'}</td>
</tr>
</tbody>
</table>
<div class="button-group">
<button
type="button"
class="btn primary"
on:click={() => {
if (!damage.opponent) {
damage.opponent = defaultOpponent();
}
editingUserIndex = index;
}}
>
<i class="fas fa-edit"></i>
{damage.opponent?.id ? $t('edit') : $t('edit')}
</button>
</div>
</div>
</details>
<input
hidden
name={`car[damages][${index}][insurance][id]`}
value={damage.insurance.id}
/>
<input hidden name={`car[damages][${index}][insurance][start_date]`} value="" />
<input hidden name={`car[damages][${index}][insurance][end_date]`} value="" />
<InputField
name="car[damages][{index}][insurance][company]"
label={$t('insurance')}
bind:value={damage.insurance.company}
placeholder={$t('placeholder.company')}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][insurance][reference]"
label={$t('insurance_reference')}
bind:value={damage.insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][notes]"
type="textarea"
label={$t('notes')}
bind:value={damage.notes}
placeholder={$t('placeholder.notes')}
rows={10}
/>
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
<button
type="button"
class="btn btn-delete danger"
on:click={() => {
if (
confirm(
$t('dialog.damage_deletion', {
values: {
name: damage.name
}
})
)
) {
car.damages = car.damages.filter((_, i) => i !== index);
}
}}
>
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
</details>
{/each}
</div>
{#if hasPrivilige(editor, PERMISSIONS.Create)}
<button
type="button"
class="btn primary"
on:click={() => {
car.damages = [...car.damages, defaultDamage()];
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
<div class="button-container">
{#if isUpdating}
@@ -211,7 +557,81 @@
</form>
{/if}
{#if editingUserIndex !== null}
<Modal on:close={close}>
<UserEditForm
{form}
submit_form={false}
subscriptions={null}
licence_categories={null}
{editor}
bind:user={car.damages[editingUserIndex].opponent}
on:cancel={() => (editingUserIndex = null)}
on:close={() => {
car.damages = car.damages;
editingUserIndex = null;
}}
/>
</Modal>
{/if}
<style>
.accordion-item {
border: none;
background: var(--surface0);
margin-bottom: 0.5rem;
border-radius: 8px;
overflow: hidden;
}
.accordion-header {
display: flex;
padding: 1rem;
cursor: pointer;
font-family: 'Roboto Mono', monospace;
color: var(--text);
background: var(--surface1);
transition: background-color 0.2s ease-in-out;
}
.accordion-header:hover {
background: var(--surface2);
}
.accordion-content {
padding: 1rem;
background: var(--surface0);
border-top: 1px solid var(--surface1);
}
.accordion-content .table {
width: 100%;
border-collapse: collapse;
font-family: 'Roboto Mono', monospace;
}
.accordion-content .table th,
.accordion-content .table td {
padding: 0.75rem;
border-bottom: 1px solid #2f2f2f;
text-align: left;
}
.accordion-content .table th {
color: var(--subtext1);
}
.accordion-content .table td {
color: var(--text);
}
.button-container button.active {
background-color: var(--mauve);
border-color: var(--mauve);
color: var(--base);
}
.btn-delete {
margin-left: auto;
}
.tab-content {
padding: 1rem;
border-radius: 0 0 3px 3px;
@@ -219,6 +639,16 @@
border: 1px solid var(--surface1);
margin-top: 1rem;
}
.tab-content h4 {
text-align: center;
padding: 0.75rem;
margin: 1rem 0;
color: var(--lavender);
font-family: 'Roboto Mono', monospace;
font-weight: 500;
letter-spacing: 0.5px;
}
.button-container {
display: flex;
justify-content: space-between;

View File

@@ -50,9 +50,9 @@
let inputValue = target.value;
if (toUpperCase) {
inputValue = inputValue.toUpperCase();
target.value = inputValue; // Update the input field value
}
value = inputValue;
target.value = inputValue; // Update the input field value
value = inputValue.trim();
}
}

View File

@@ -44,7 +44,7 @@
<form class="content" action="?/updateSubscription" method="POST" use:enhance={handleUpdate}>
<input name="susbscription[id]" type="hidden" bind:value={subscription.id} />
<h1 class="step-title" style="text-align: center;">
{subscription.id ? $t('subscription.edit') : $t('subscription.create')}
{subscription.id ? $t('subscriptions.edit') : $t('subscriptions.create')}
</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
@@ -60,7 +60,7 @@
<div class="tab-content" style="display: block">
<InputField
name="subscription[name]"
label={$t('subscription.name')}
label={$t('subscriptions.name')}
bind:value={subscription.name}
placeholder={$t('placeholder.subscription_name')}
required={true}
@@ -77,7 +77,7 @@
<InputField
name="subscription[conditions]"
type="textarea"
label={$t('subscription.conditions')}
label={$t('subscriptions.conditions')}
bind:value={subscription.conditions}
placeholder={$t('placeholder.subscription_conditions')}
readonly={subscription.id > 0}
@@ -85,7 +85,7 @@
<InputField
name="subscription[monthly_fee]"
type="number"
label={$t('subscription.monthly_fee')}
label={$t('subscriptions.monthly_fee')}
bind:value={subscription.monthly_fee}
placeholder={$t('placeholder.subscription_monthly_fee')}
required={true}
@@ -94,7 +94,7 @@
<InputField
name="subscription[hourly_rate]"
type="number"
label={$t('subscription.hourly_rate')}
label={$t('subscriptions.hourly_rate')}
bind:value={subscription.hourly_rate}
required={true}
readonly={subscription.id > 0}
@@ -102,14 +102,14 @@
<InputField
name="subscription[included_hours_per_year]"
type="number"
label={$t('subscription.included_hours_per_year')}
label={$t('subscriptions.included_hours_per_year')}
bind:value={subscription.included_hours_per_year}
readonly={subscription.id > 0}
/>
<InputField
name="included_hours_per_month"
type="number"
label={$t('subscription.included_hours_per_month')}
label={$t('subscriptions.included_hours_per_month')}
bind:value={subscription.included_hours_per_month}
readonly={subscription.id > 0}
/>

View File

@@ -6,22 +6,29 @@
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
import { PERMISSIONS } from '$lib/utils/constants';
import { defaultLicence } from '$lib/utils/defaults';
// import { defaultBankAccount, defaultLicence, defaultMembership } from '$lib/utils/defaults';
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form;
/** @type {App.Locals['subscriptions']}*/
/** @type {App.Locals['subscriptions'] | null}*/
export let subscriptions;
/** @type {App.Locals['user']} */
export let user;
// Ensure licence is initialized before passing to child
$: if (user && !user.licence) {
user.licence = defaultLicence();
}
export let submit_form = true;
// Ensure licence is initialized before passing to child
// $: if (user && !user.licence) {
// user.licence = defaultLicence();
// }
// $: if (user && !user.membership) {
// user.membership = defaultMembership();
// }
// $: if (user && !user.bank_account) {
// user.bank_account = defaultBankAccount();
// }
/** @type {App.Locals['user']} */
export let editor;
@@ -29,8 +36,10 @@
// $: isNewUser = user === null;
$: isLoading = user === undefined;
/** @type {App.Locals['licence_categories']} */
$: if (user != null) {
console.log(user);
}
/** @type {App.Locals['licence_categories'] | null} */
export let licence_categories;
const userStatusOptions = [
@@ -41,6 +50,7 @@
{ value: 5, label: $t('userStatus.5'), color: '--red' } // Red for "Deaktiviert"
];
const userRoleOptions = [
{ value: -1, label: $t('userRole.-1'), color: '--red' }, // Red for "Opponent"
{ value: 0, label: $t('userRole.0'), color: '--subtext1' }, // Grey for "Nicht verifiziert"
{ value: 1, label: $t('userRole.1'), color: '--light-green' }, // Light green for "Verifiziert"
{ value: 2, label: $t('userRole.2'), color: '--green' }, // Light green for "Verifiziert"
@@ -60,24 +70,26 @@
];
const dispatch = createEventDispatcher();
const TABS = hasPrivilige(user, PERMISSIONS.Member)
? ['profile', 'licence', 'membership', 'bankaccount']
: ['profile', 'bankaccount', 'membership'];
let activeTab = TABS[0];
/** @type { (keyof user)[] } */
const TABS = ['membership', 'licence', 'bank_account'];
let activeTab = 'profile';
let isUpdating = false,
password = '',
confirm_password = '';
/** @type {Object.<string, App.Locals['licence_categories']>} */
$: groupedCategories = groupCategories(licence_categories);
$: subscriptionModelOptions = subscriptions.map((sub) => ({
value: sub?.name ?? '',
label: sub?.name ?? ''
}));
$: selectedSubscriptionModel =
subscriptions.find((sub) => sub?.name === user.membership?.subscription_model.name) || null;
$: groupedCategories = licence_categories ? groupCategories(licence_categories) : {};
$: subscriptionOptions = subscriptions
? subscriptions.map((sub) => ({
value: sub?.name ?? '',
label: sub?.name ?? ''
}))
: [];
$: selectedSubscription = subscriptions
? subscriptions.find((sub) => sub?.name === user.membership?.subscription.name) || null
: null;
/**
* creates groups of categories depending on the first letter
* @param {App.Locals['licence_categories']} categories - the categories to sort and group
@@ -99,17 +111,25 @@
);
}
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */
const handleUpdate = async () => {
/** @type {import('@sveltejs/kit').SubmitFunction} */
const handleUpdate = ({ cancel }) => {
if (!submit_form) {
cancel();
dispatch('close');
return;
}
isUpdating = true;
return async ({ result }) => {
isUpdating = false;
if (result.type === 'success' || result.type === 'redirect') {
dispatch('close');
} else {
document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' });
}
await applyAction(result);
console.log('submitting');
return submit_form ? await applyAction(result) : undefined;
};
};
</script>
@@ -117,7 +137,18 @@
{#if isLoading}
<SmallLoader width={30} message={$t('loading.user_data')} />
{:else if user}
<form class="content" action="?/updateUser" method="POST" use:enhance={handleUpdate}>
<form
class="content"
action="?/updateUser"
method="POST"
use:enhance={handleUpdate}
on:submit={(/** @type{SubmitEvent}*/ e) => {
if (!submit_form) {
e.preventDefault();
dispatch('close');
}
}}
>
<input name="user[id]" type="hidden" bind:value={user.id} />
<h1 class="step-title" style="text-align: center;">
{user.id ? $t('user.edit') : $t('user.create')}
@@ -145,26 +176,38 @@
{/if}
<div class="button-container">
<button
type="button"
class="button-dark"
class:active={activeTab === 'profile'}
on:click={() => (activeTab = 'profile')}
>
{$t('profile')}
</button>
{#each TABS as tab}
<button
type="button"
class="button-dark"
class:active={activeTab === tab}
on:click={() => (activeTab = tab)}
>
{$t(tab)}
</button>
{#if user[tab] != null}
<button
type="button"
class="button-dark"
class:active={activeTab === tab}
on:click={() => (activeTab = tab)}
>
{$t('user.' + tab)}
</button>
{/if}
{/each}
</div>
<div class="tab-content" style="display: {activeTab === 'profile' ? 'block' : 'none'}">
<InputField
name="user[status]"
type="select"
label={$t('status')}
bind:value={user.status}
options={userStatusOptions}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[status]"
type="select"
label={$t('status')}
bind:value={user.status}
options={userStatusOptions}
readonly={readonlyUser}
/>
{/if}
{#if hasPrivilige(editor, PERMISSIONS.Super)}
<InputField
name="user[role_id]"
@@ -229,16 +272,14 @@
bind:value={user.phone}
placeholder={$t('placeholder.phone')}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[dateofbirth]"
type="date"
label={$t('user.dateofbirth')}
bind:value={user.dateofbirth}
placeholder={$t('placeholder.dateofbirth')}
readonly={readonlyUser}
/>
{/if}
<InputField
name="user[dateofbirth]"
type="date"
label={$t('user.dateofbirth')}
bind:value={user.dateofbirth}
placeholder={$t('placeholder.dateofbirth')}
readonly={readonlyUser}
/>
<InputField
name="user[address]"
label={$t('address')}
@@ -271,7 +312,7 @@
{/if}
</div>
{#if hasPrivilige(user, PERMISSIONS.Member)}
{#if hasPrivilige(user, PERMISSIONS.Member) && user.licence}
<div class="tab-content" style="display: {activeTab === 'licence' ? 'block' : 'none'}">
<InputField
name="user[licence][status]"
@@ -342,130 +383,137 @@
</div>
</div>
{/if}
<div class="tab-content" style="display: {activeTab === 'membership' ? 'block' : 'none'}">
<InputField
name="user[membership][status]"
type="select"
label={$t('status')}
bind:value={user.membership.status}
options={membershipStatusOptions}
readonly={readonlyUser}
/>
<InputField
name="user[membership][subscription_model][name]"
type="select"
label={$t('subscription.subscription')}
bind:value={user.membership.subscription_model.name}
options={subscriptionModelOptions}
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)}
/>
<div class="subscription-info">
{#if hasPrivilige(user, PERMISSIONS.Member)}
{#if user.membership}
<div
class="tab-content"
style="display: {activeTab === 'membership' && subscriptions ? 'block' : 'none'}"
>
<InputField
name="user[membership][status]"
type="select"
label={$t('status')}
bind:value={user.membership.status}
options={membershipStatusOptions}
readonly={readonlyUser}
/>
<InputField
name="user[membership][subscription][name]"
type="select"
label={$t('subscriptions.subscription')}
bind:value={user.membership.subscription.name}
options={subscriptionOptions}
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)}
/>
<div class="subscription-info">
{#if hasPrivilige(user, PERMISSIONS.Member)}
<div class="subscription-column">
<p>
<strong>{$t('subscriptions.monthly_fee')}:</strong>
{selectedSubscription?.monthly_fee || '-'}
</p>
<p>
<strong>{$t('subscriptions.hourly_rate')}:</strong>
{selectedSubscription?.hourly_rate || '-'}
</p>
{#if selectedSubscription?.included_hours_per_year}
<p>
<strong>{$t('subscriptions.included_hours_per_year')}:</strong>
{selectedSubscription?.included_hours_per_year}
</p>
{/if}
{#if selectedSubscription?.included_hours_per_month}
<p>
<strong>{$t('subscriptions.included_hours_per_month')}:</strong>
{selectedSubscription?.included_hours_per_month}
</p>
{/if}
</div>
{/if}
<div class="subscription-column">
<p>
<strong>{$t('subscription.monthly_fee')}:</strong>
{selectedSubscriptionModel?.monthly_fee || '-'}
<strong>{$t('details')}:</strong>
{selectedSubscription?.details || '-'}
</p>
<p>
<strong>{$t('subscription.hourly_rate')}:</strong>
{selectedSubscriptionModel?.hourly_rate || '-'}
</p>
{#if selectedSubscriptionModel?.included_hours_per_year}
{#if selectedSubscription?.conditions}
<p>
<strong>{$t('subscription.included_hours_per_year')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year}
</p>
{/if}
{#if selectedSubscriptionModel?.included_hours_per_month}
<p>
<strong>{$t('subscription.included_hours_per_month')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month}
<strong>{$t('subscriptions.conditions')}:</strong>
{selectedSubscription?.conditions}
</p>
{/if}
</div>
{/if}
<div class="subscription-column">
<p>
<strong>{$t('details')}:</strong>
{selectedSubscriptionModel?.details || '-'}
</p>
{#if selectedSubscriptionModel?.conditions}
<p>
<strong>{$t('subscription.conditions')}:</strong>
{selectedSubscriptionModel?.conditions}
</p>
{/if}
</div>
</div>
<InputField
name="user[membership][start_date]"
type="date"
label={$t('start')}
bind:value={user.membership.start_date}
placeholder={$t('placeholder.start_date')}
readonly={readonlyUser}
/>
<InputField
name="user[membership][end_date]"
type="date"
label={$t('end')}
bind:value={user.membership.end_date}
placeholder={$t('placeholder.end_date')}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[membership][parent_member_id]"
type="number"
label={$t('parent_member_id')}
bind:value={user.membership.parent_member_id}
placeholder={$t('placeholder.parent_member_id')}
name="user[membership][start_date]"
type="date"
label={$t('start')}
bind:value={user.membership.start_date}
placeholder={$t('placeholder.start_date')}
readonly={readonlyUser}
/>
{/if}
</div>
<div class="tab-content" style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}">
<InputField
name="user[bank_account][account_holder_name]"
label={$t('bank_account_holder')}
bind:value={user.bank_account.account_holder_name}
placeholder={$t('placeholder.bank_account_holder')}
/>
<InputField
name="user[bank_account][bank_name]"
label={$t('bank_name')}
bind:value={user.bank_account.bank}
placeholder={$t('placeholder.bank_name')}
/>
<InputField
name="user[bank_account][iban]"
label={$t('iban')}
bind:value={user.bank_account.iban}
placeholder={$t('placeholder.iban')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][bic]"
label={$t('bic')}
bind:value={user.bank_account.bic}
placeholder={$t('placeholder.bic')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][mandate_reference]"
label={$t('mandate_reference')}
bind:value={user.bank_account.mandate_reference}
placeholder={$t('placeholder.mandate_reference')}
readonly={readonlyUser}
/>
<InputField
name="user[bank_account][mandate_date_signed]"
label={$t('mandate_date_signed')}
type="date"
bind:value={user.bank_account.mandate_date_signed}
readonly={true}
/>
</div>
<InputField
name="user[membership][end_date]"
type="date"
label={$t('end')}
bind:value={user.membership.end_date}
placeholder={$t('placeholder.end_date')}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField
name="user[membership][parent_member_id]"
type="number"
label={$t('parent_member_id')}
bind:value={user.membership.parent_member_id}
placeholder={$t('placeholder.parent_member_id')}
readonly={readonlyUser}
/>
{/if}
</div>
{/if}
{#if user.bank_account}
<div class="tab-content" style="display: {activeTab === 'bank_account' ? 'block' : 'none'}">
<InputField
name="user[bank_account][account_holder_name]"
label={$t('bank_account_holder')}
bind:value={user.bank_account.account_holder_name}
placeholder={$t('placeholder.bank_account_holder')}
/>
<InputField
name="user[bank_account][bank_name]"
label={$t('bank_name')}
bind:value={user.bank_account.bank}
placeholder={$t('placeholder.bank_name')}
/>
<InputField
name="user[bank_account][iban]"
label={$t('iban')}
bind:value={user.bank_account.iban}
placeholder={$t('placeholder.iban')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][bic]"
label={$t('bic')}
bind:value={user.bank_account.bic}
placeholder={$t('placeholder.bic')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][mandate_reference]"
label={$t('mandate_reference')}
bind:value={user.bank_account.mandate_reference}
placeholder={$t('placeholder.mandate_reference')}
readonly={readonlyUser}
/>
<InputField
name="user[bank_account][mandate_date_signed]"
label={$t('mandate_date_signed')}
type="date"
bind:value={user.bank_account.mandate_date_signed}
readonly={true}
/>
</div>
{/if}
<div class="button-container">
{#if isUpdating}
<SmallLoader width={30} message={$t('loading.updating')} />

View File

@@ -7,11 +7,12 @@ export default {
5: 'Passiv'
},
userRole: {
'-1': 'Unfallgegner',
0: 'Sponsor',
1: 'Mitglied',
2: 'Betrachter',
4: 'Bearbeiter',
8: 'Adm/endinistrator'
8: 'Administrator'
},
placeholder: {
car_name: 'Hat das Fahrzeug einen Namen?',
@@ -75,7 +76,7 @@ export default {
validation: {
invalid: 'ungültig',
invalid_user_id: 'Nutzer ID ungültig',
invalid_subscription_model: 'Model nicht gefunden',
invalid_subscription: 'Model nicht gefunden',
user_not_found: '{field} konnte nicht gefunden werden',
invalid_user_data: 'Nutzerdaten ungültig',
user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
@@ -130,6 +131,7 @@ export default {
edit: 'Nutzer bearbeiten',
create: 'Nutzer erstellen',
user: 'Nutzer',
member: 'Mitglied',
management: 'Mitgliederverwaltung',
id: 'Mitgliedsnr',
first_name: 'Vorname',
@@ -137,11 +139,14 @@ export default {
phone: 'Telefonnummer',
dateofbirth: 'Geburtstag',
email: 'Email',
membership: 'Mitgliedschaft',
bank_account: 'Kontodaten',
status: 'Status',
role: 'Nutzerrolle',
supporter: 'Sponsor'
supporter: 'Sponsor',
opponent: 'Unfallgegner'
},
subscription: {
subscriptions: {
name: 'Modellname',
edit: 'Modell bearbeiten',
create: 'Modell erstellen',
@@ -165,9 +170,14 @@ export default {
end_date: 'Leasingende',
leasing_rate: 'Leasingrate'
},
insurances: {
edit: 'Daten bearbeiten',
create: 'Versicherung erstellen'
},
loading: {
user_data: 'Lade Nutzerdaten',
subscription_data: 'Lade Modelldaten',
insurance_data: 'Lade Versicherungsdaten',
car_data: 'Lade Fahrzeugdaten',
please_wait: 'Bitte warten...',
updating: 'Aktualisiere...'
@@ -176,15 +186,21 @@ export default {
user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?',
subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?',
car_deletion: 'Soll das Fahrzeug {name} wirklich gelöscht werden?',
insurance_deletion: 'Soll die Versicherung {name} wirklich gelöscht werden?',
damage_deletion: 'Soll der Schaden {name} wirklich gelöscht werden?',
backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?'
},
cancel: 'Abbrechen',
confirm: 'Bestätigen',
actions: 'Aktionen',
create: 'Hinzufügen',
edit: 'Bearbeiten',
delete: 'Löschen',
not_set: 'Nicht gesetzt',
noone: 'Niemand',
search: 'Suche:',
name: 'Name',
date: 'Datum',
price: 'Preis',
color: 'Farbe',
grant_backend_access: 'Backend Zugriff gewähren',
@@ -192,7 +208,7 @@ export default {
supporter: 'Sponsoren',
mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: 'Führerscheinklassen',
subscription_model: 'Mitgliedschatfsmodell',
subscription: 'Mitgliedschatfsmodell',
licence: 'Führerschein',
licence_number: 'Führerscheinnummer',
insurance: 'Versicherung',
@@ -218,8 +234,6 @@ export default {
login: 'Anmeldung',
profile: 'Profil',
cars: 'Fahrzeuge',
membership: 'Mitgliedschaft',
bankaccount: 'Kontodaten',
status: 'Status',
start: 'Beginn',
end: 'Ende',

View File

@@ -69,7 +69,7 @@ export default {
validation: {
invalid: 'Invalid',
invalid_user_id: 'Invalid user ID',
invalid_subscription_model: 'Model not found',
invalid_subscription: 'Model not found',
user_not_found: '{field} could not be found',
invalid_user_data: 'Invalid user data',
user_not_found_or_wrong_password: 'Does not exist or wrong password',
@@ -128,7 +128,7 @@ export default {
role: 'User Role',
supporter: 'Sponsor'
},
subscription: {
subscriptions: {
name: 'Model Name',
edit: 'Edit Model',
create: 'Create Model',
@@ -160,7 +160,7 @@ export default {
supporter: 'Sponsors',
mandate_date_signed: 'Mandate Signing Date',
licence_categories: 'Drivers licence Categories',
subscription_model: 'Membership Model',
subscription: 'Membership Model',
licence: 'Drivers licence',
licence_number: 'Drivers licence Number',
issued_date: 'Issue Date',
@@ -182,7 +182,7 @@ export default {
login: 'Login',
profile: 'Profile',
membership: 'Membership',
bankaccount: 'Bank Account',
bank_account: 'Bank Account',
status: 'Status',
start: 'Start',
end: 'End',

View File

@@ -10,5 +10,3 @@ export const PERMISSIONS = {
Delete: 4,
Super: 8
};
export const SUPPORTER_SUBSCRIPTION_MODEL_NAME = 'Keins';

View File

@@ -1,7 +1,5 @@
// src/lib/utils/defaults.js
import { SUPPORTER_SUBSCRIPTION_MODEL_NAME } from './constants';
/**
* @returns {App.Types['subscription']}
*/
@@ -28,7 +26,7 @@ export function defaultMembership() {
start_date: '',
end_date: '',
parent_member_id: 0,
subscription_model: defaultSubscription()
subscription: defaultSubscription()
};
}
@@ -79,8 +77,6 @@ export function defaultUser() {
company: '',
dateofbirth: '',
notes: '',
profile_picture: '',
payment_status: 0,
status: 1,
role_id: 1,
membership: defaultMembership(),
@@ -93,31 +89,25 @@ export function defaultUser() {
* @returns {App.Locals['user']}
*/
export function defaultSupporter() {
let supporter = {
id: 0,
email: '',
first_name: '',
last_name: '',
password: '',
phone: '',
address: '',
zip_code: '',
city: '',
company: '',
dateofbirth: '',
notes: '',
profile_picture: '',
payment_status: 0,
status: 5,
role_id: 0,
membership: defaultMembership(),
licence: defaultLicence(),
bank_account: defaultBankAccount()
};
supporter.membership.subscription_model.name = SUPPORTER_SUBSCRIPTION_MODEL_NAME;
let supporter = defaultUser();
supporter.status = 5;
supporter.role_id = 0;
supporter.licence = null;
supporter.membership = null;
return supporter;
}
/**
* @returns {App.Locals['user']}
*/
export function defaultOpponent() {
let opponent = defaultUser();
opponent.status = 5;
opponent.role_id = -1;
opponent.licence = null;
opponent.membership = null;
return opponent;
}
/**
* @returns {App.Types['location']}
*/
@@ -134,8 +124,11 @@ export function defaultLocation() {
export function defaultDamage() {
return {
id: 0,
opponent: defaultUser(),
insurance: null,
name: '',
opponent: defaultOpponent(),
driver_id: -1,
insurance: defaultInsurance(),
date: '',
notes: ''
};
}
@@ -161,7 +154,7 @@ export function defaultCar() {
return {
id: 0,
name: '',
status: '',
status: 0,
brand: '',
model: '',
price: 0,

View File

@@ -72,7 +72,7 @@ export function isEmpty(obj) {
* @returns string
*/
export function toRFC3339(dateString) {
if (!dateString) dateString = '0001-01-01T00:00:00.000Z';
if (!dateString || dateString == '') dateString = '0001-01-01T00:00:00.000Z';
const date = new Date(dateString);
return date.toISOString();
}

View File

@@ -1,3 +1,4 @@
import { defaultBankAccount, defaultMembership } from './defaults';
import { toRFC3339 } from './helpers';
/**
@@ -24,20 +25,18 @@ export function formDataToObject(formData) {
// console.log('Current object state:', JSON.stringify(current));
for (let i = 0; i < keys.length - 1; i++) {
/**
* Create nested object if it doesn't exist
* @type {Record<string, any>}
* @description Ensures proper nesting structure for user data fields
* @example
* // For input name="user[membership][status]"
* // Creates: { user: { membership: { status: value } } }
*/
current[keys[i]] = current[keys[i]] || {};
const currentKey = keys[i];
const nextKey = keys[i + 1];
const isNextKeyArrayIndex = !isNaN(Number(nextKey));
if (!current[currentKey]) {
// If next key is a number, initialize an array, otherwise an object
current[currentKey] = isNextKeyArrayIndex ? [] : {};
}
/**
* Move to the next level of the object
* @type {Record<string, any>}
*/
current = current[keys[i]];
current = current[currentKey];
}
const lastKey = keys[keys.length - 1];
@@ -50,7 +49,20 @@ export function formDataToObject(formData) {
current[lastKey].push(value);
}
} else {
current[lastKey] = value;
if (Array.isArray(current)) {
// If current is an array, lastKey should be the index
const index = parseInt(lastKey);
current[index] = current[index] || {};
if (keys.length > 2) {
// For nested properties within array elements
const propertyKey = keys[keys.length - 1];
current[index][propertyKey] = value;
} else {
current[index] = value;
}
} else {
current[lastKey] = value;
}
}
}
@@ -58,82 +70,86 @@ export function formDataToObject(formData) {
}
/**
* Processes the raw form data into the expected user data structure
* @param {{ object: Partial<App.Locals['user']>, confirm_password: string} } rawData - The raw form data object
* @returns {{ user: Partial<App.Locals['user']> }} Processed user data
* Processes the raw form data into the expected membership data structure
* @param { App.Types['membership'] } membership - The raw form data object
* @returns {App.Types['membership']} Processed membership data
*/
export function processUserFormData(rawData) {
/** @type {{ user: Partial<App.Locals['user']> }} */
export function processMembershipFormData(membership) {
return {
id: Number(membership.id) || 0,
status: Number(membership.status),
start_date: toRFC3339(String(membership.start_date || '')),
end_date: toRFC3339(String(membership.end_date || '')),
parent_member_id: Number(membership.parent_member_id) || 0,
subscription: processSubscriptionFormData(membership.subscription)
};
}
/**
* Processes the raw form data into the expected licence data structure
* @param { App.Types['licence'] } licence - The raw form data object
* @returns {App.Types['licence']} Processed licence data
*/
export function processLicenceFormData(licence) {
return {
id: Number(licence?.id) || 0,
status: Number(licence?.status),
number: String(licence?.number || ''),
issued_date: toRFC3339(String(licence?.issued_date || '')),
expiration_date: toRFC3339(String(licence?.expiration_date || '')),
country: String(licence?.country || ''),
categories: licence?.categories || []
};
}
/**
* Processes the raw form data into the expected bank_account data structure
* @param { App.Types['bankAccount'] } bank_account - The raw form data object
* @returns {App.Types['bankAccount']} Processed bank_account data
*/
export function processBankAccountFormData(bank_account) {
{
return {
id: Number(bank_account?.id) || 0,
account_holder_name: String(bank_account?.account_holder_name || ''),
bank: String(bank_account?.bank || ''),
iban: String(bank_account?.iban || ''),
bic: String(bank_account?.bic || ''),
mandate_reference: String(bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(String(bank_account?.mandate_date_signed || ''))
};
}
}
/**
* Processes the raw form data into the expected user data structure
* @param { Partial<App.Locals['user']> } user - The raw form data object
* @returns {App.Locals['user']} Processed user data
*/
export function processUserFormData(user) {
/** @type {App.Locals['user']} */
let processedData = {
user: {
id: Number(rawData.object.id) || 0,
status: Number(rawData.object.status),
role_id: Number(rawData.object.role_id),
first_name: String(rawData.object.first_name),
last_name: String(rawData.object.last_name),
email: String(rawData.object.email),
phone: String(rawData.object.phone || ''),
company: String(rawData.object.company || ''),
dateofbirth: toRFC3339(String(rawData.object.dateofbirth || '')),
address: String(rawData.object.address || ''),
zip_code: String(rawData.object.zip_code || ''),
city: String(rawData.object.city || ''),
notes: String(rawData.object.notes || ''),
profile_picture: String(rawData.object.profile_picture || ''),
membership: {
id: Number(rawData.object.membership?.id) || 0,
status: Number(rawData.object.membership?.status),
start_date: toRFC3339(String(rawData.object.membership?.start_date || '')),
end_date: toRFC3339(String(rawData.object.membership?.end_date || '')),
parent_member_id: Number(rawData.object.membership?.parent_member_id) || 0,
subscription_model: {
id: Number(rawData.object.membership?.subscription_model?.id) || 0,
name: String(rawData.object.membership?.subscription_model?.name) || '',
details: String(rawData.object.membership?.subscription_model?.details) || '',
conditions: String(rawData.object.membership?.subscription_model?.conditions) || '',
hourly_rate: Number(rawData.object.membership?.subscription_model?.hourly_rate) || 0,
monthly_fee: Number(rawData.object.membership?.subscription_model?.monthly_fee) || 0,
included_hours_per_month:
Number(rawData.object.membership?.subscription_model?.included_hours_per_month) || 0,
included_hours_per_year:
Number(rawData.object.membership?.subscription_model?.included_hours_per_year) || 0
}
},
licence: {
id: Number(rawData.object.licence?.id) || 0,
status: Number(rawData.object.licence?.status),
number: String(rawData.object.licence?.number || ''),
issued_date: toRFC3339(String(rawData.object.licence?.issued_date || '')),
expiration_date: toRFC3339(String(rawData.object.licence?.expiration_date || '')),
country: String(rawData.object.licence?.country || ''),
categories: rawData.object.licence?.categories || []
},
bank_account: {
id: Number(rawData.object.bank_account?.id) || 0,
account_holder_name: String(rawData.object.bank_account?.account_holder_name || ''),
bank: String(rawData.object.bank_account?.bank || ''),
iban: String(rawData.object.bank_account?.iban || ''),
bic: String(rawData.object.bank_account?.bic || ''),
mandate_reference: String(rawData.object.bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(
String(rawData.object.bank_account?.mandate_date_signed || '')
)
}
}
id: Number(user.id) || 0,
status: Number(user.status),
role_id: Number(user.role_id),
first_name: String(user.first_name),
last_name: String(user.last_name),
password: String(user.password) || '',
email: String(user.email),
phone: String(user.phone || ''),
company: String(user.company || ''),
dateofbirth: toRFC3339(String(user.dateofbirth || '')),
address: String(user.address || ''),
zip_code: String(user.zip_code || ''),
city: String(user.city || ''),
notes: String(user.notes || ''),
membership: processMembershipFormData(user.membership ? user.membership : defaultMembership()),
licence: user.licence ? processLicenceFormData(user.licence) : null,
bank_account: processBankAccountFormData(
user.bank_account ? user.bank_account : defaultBankAccount()
)
};
// console.log('Categories: --------');
// console.dir(rawData.object.licence);
if (
rawData.object.password &&
rawData.confirm_password &&
rawData.object.password === rawData.confirm_password &&
rawData.object.password.trim() !== ''
) {
processedData.user.password = rawData.object.password;
}
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
@@ -143,22 +159,20 @@ export function processUserFormData(rawData) {
/**
* Processes the raw form data into the expected subscription data structure
* @param {{ object: Partial<App.Types['subscription']>, confirm_password: string }} rawData - The raw form data object
* @returns {{ subscription: Partial<App.Types['subscription']> }} Processed user data
* @param {Partial<App.Types['subscription']>} subscription - The raw form data object
* @returns {App.Types['subscription']} Processed user data
*/
export function processSubscriptionFormData(rawData) {
/** @type {{ subscription: Partial<App.Types['subscription']> }} */
export function processSubscriptionFormData(subscription) {
/** @type {Partial<App.Types['subscription']>} */
let processedData = {
subscription: {
id: Number(rawData.object.id) || 0,
name: String(rawData.object.name) || '',
details: String(rawData.object.details) || '',
conditions: String(rawData.object.conditions) || '',
hourly_rate: Number(rawData.object.hourly_rate) || 0,
monthly_fee: Number(rawData.object.monthly_fee) || 0,
included_hours_per_month: Number(rawData.object.included_hours_per_month) || 0,
included_hours_per_year: Number(rawData.object.included_hours_per_year) || 0
}
id: Number(subscription.id) || 0,
name: String(subscription.name) || '',
details: String(subscription.details) || '',
conditions: String(subscription.conditions) || '',
hourly_rate: Number(subscription.hourly_rate) || 0,
monthly_fee: Number(subscription.monthly_fee) || 0,
included_hours_per_month: Number(subscription.included_hours_per_month) || 0,
included_hours_per_year: Number(subscription.included_hours_per_year) || 0
};
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
@@ -166,39 +180,85 @@ export function processSubscriptionFormData(rawData) {
console.dir(clean);
return clean;
}
/**
* Processes the raw form data into the expected insurance data structure
* @param {App.Types['insurance']} insurance - The raw form data object
* @returns {App.Types['insurance']} Processed user data
*/
export function processInsuranceFormData(insurance) {
return {
id: Number(insurance.id) || 0,
company: String(insurance.company) || '',
reference: String(insurance.reference) || '',
start_date: toRFC3339(String(insurance.start_date) || '') || '',
end_date: toRFC3339(String(insurance.end_date) || '') || '',
notes: String(insurance.notes) || ''
};
}
/**
* Processes the raw form data into the expected car data structure
* @param {Partial<App.Types['car']>} car - The raw form data object
* @returns {App.Types['car']} Processed user data
*/
export function processCarFormData(car) {
console.dir(car);
/** @type {App.Types['car']} */
let processedData = {
id: Number(car.id) || 0,
name: String(car.name) || '',
status: Number(car.status) || 0,
brand: String(car.brand) || '',
model: String(car.model) || '',
price: Number(car.price) || 0,
rate: Number(car.rate) || 0,
licence_plate: String(car.licence_plate),
start_date: 'start_date' in car ? toRFC3339(String(car.start_date) || '') : '',
end_date: 'end_date' in car ? toRFC3339(String(car.end_date) || '') : '',
color: String(car.color) || '',
notes: String(car.notes) || '',
location:
'location' in car
? {
latitude: Number(car.location?.latitude) || 0,
longitude: Number(car.location?.longitude) || 0
}
: {
latitude: 0,
longitude: 0
},
damages: /** @type {App.Types['damage'][]} */ ([]),
insurances: /** @type {App.Types['insurance'][]} */ ([])
};
car.insurances?.forEach((insurance) => {
processedData.insurances.push(processInsuranceFormData(insurance));
});
car.damages?.forEach((damage) => {
console.dir(damage);
processedData.damages.push(processDamageFormData(damage));
});
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
console.dir(clean);
return clean;
}
/**
* Processes the raw form data into the expected car data structure
* @param {{ object: Partial<App.Types['car']>, confirm_password: string }} rawData - The raw form data object
* @returns {{ car: Partial<App.Types['car']> }} Processed user data
* Processes the raw form data into the expected damage data structure
* @param { App.Types['damage'] } damage - The raw form data object
* @returns {App.Types['damage']} Processed damage data
*/
export function processCarFormData(rawData) {
/** @type {{ car: Partial<App.Types['car']> }} */
let processedData = {
car: {
id: Number(rawData.object.id) || 0,
name: String(rawData.object.name) || '',
status: Number(rawData.object.status) || 0,
brand: String(rawData.object.brand) || '',
model: String(rawData.object.model) || '',
price: Number(rawData.object.price) || 0,
rate: Number(rawData.object.rate) || 0,
licence_plate: String(rawData.object.licence_plate) || '',
start_date: toRFC3339(String(rawData.object.start_date)) || '',
end_date: toRFC3339(String(rawData.object.end_date)) || '',
color: String(rawData.object.color) || '',
notes: String(rawData.object.notes) || '',
location: {
latitude: Number(rawData.object.location?.latitude) || 0,
longitude: Number(rawData.object.location?.longitude) || 0
},
damages: rawData.object.damages || [],
insurances: rawData.object.insurances || []
}
export function processDamageFormData(damage) {
return {
id: Number(damage.id) || 0,
name: String(damage.name) || '',
opponent: processUserFormData(damage.opponent),
driver_id: Number(damage.driver_id) || 0,
insurance: processInsuranceFormData(damage.insurance),
date: toRFC3339(String(damage.date) || ''),
notes: String(damage.notes) || ''
};
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
console.dir(clean);
return clean;
}

View File

@@ -30,8 +30,21 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData();
const rawData = formDataToObject(formData);
const processedData = processUserFormData(rawData);
const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
// confirm password matches and is not empty. Otherwise set password to empty string
if (
rawData.object.password &&
rawData.confirm_password &&
(rawData.object.password != rawData.confirm_password || rawData.object.password.trim() == '')
) {
rawData.object.password = '';
}
const processedData = processUserFormData(rawData.object);
// const isCreating = !processedData.user.id || processedData.user.id === 0;
// console.log('Is creating: ', isCreating);

View File

@@ -38,15 +38,23 @@ export const actions = {
let formData = await request.formData();
const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
/** @type {{object: App.Locals['user'], confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
object: /** @type {App.Locals['user']} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData);
// confirm password matches and is not empty. Otherwise set password to empty string
if (
rawData.object.password &&
rawData.confirm_password &&
(rawData.object.password != rawData.confirm_password || rawData.object.password.trim() == '')
) {
rawData.object.password = '';
}
const user = processUserFormData(rawData.object);
console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0;
console.dir(user.membership);
const isCreating = !user.id || user.id === 0;
console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/auth/users`;
@@ -58,7 +66,7 @@ export const actions = {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData)
body: JSON.stringify(user)
};
const res = await fetch(apiURL, requestOptions);
@@ -87,10 +95,11 @@ export const actions = {
updateSubscription: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
const rawData = formDataToObject(formData);
const processedData = processSubscriptionFormData(rawData);
const rawFormData = formDataToObject(formData);
const rawSubscription = /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object);
const subscription = processSubscriptionFormData(rawSubscription);
const isCreating = !processedData.subscription.id || processedData.subscription.id === 0;
const isCreating = !subscription.id || subscription.id === 0;
console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/auth/subscriptions`;
@@ -102,7 +111,7 @@ export const actions = {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData.subscription)
body: JSON.stringify(subscription)
};
const res = await fetch(apiURL, requestOptions);
@@ -127,18 +136,13 @@ export const actions = {
*/
updateCar: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
console.dir(formData);
const rawCar = /**@type {Partial<App.Types['car']>} */ (formDataToObject(formData).object);
const car = processCarFormData(rawCar);
const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Types['car']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['car']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processCarFormData(rawData);
const isCreating = !processedData.car.id || processedData.car.id === 0;
const isCreating = !car.id || car.id === 0;
console.log('Is creating: ', isCreating);
console.log('sending: ', JSON.stringify(processedData.car));
console.log('sending: ', JSON.stringify(car.damages));
const apiURL = `${BASE_API_URI}/auth/cars`;
@@ -150,7 +154,7 @@ export const actions = {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData.car)
body: JSON.stringify(car)
};
const res = await fetch(apiURL, requestOptions);
@@ -163,6 +167,8 @@ export const actions = {
const response = await res.json();
console.log('Server success response:', response);
console.log('Server opponent response:', response.damages[0]?.opponent);
console.log('Server insurance response:', response.damages[0]?.insurance);
throw redirect(303, `${base}/auth/admin/users`);
},
@@ -178,12 +184,8 @@ export const actions = {
let formData = await request.formData();
const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData);
/** @type {Partial<App.Locals['user']>} */
const rawUser = /** @type {Partial<App.Locals['user']>} */ (rawFormData.object);
const apiURL = `${BASE_API_URI}/auth/users`;
@@ -195,7 +197,7 @@ export const actions = {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData)
body: JSON.stringify({ id: Number(rawUser.id) })
};
const res = await fetch(apiURL, requestOptions);
@@ -216,14 +218,15 @@ export const actions = {
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current subscription
* @returns
*/
subscriptionDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
const rawData = formDataToObject(formData);
const processedData = processSubscriptionFormData(rawData);
/** @type {Partial<App.Types['subscription']>} */
const subscription = rawData.object;
const apiURL = `${BASE_API_URI}/auth/subscriptions`;
@@ -235,7 +238,45 @@ export const actions = {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData.subscription)
body: JSON.stringify({ id: Number(subscription.id), name: subscription.name })
};
const res = await fetch(apiURL, requestOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json();
console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`);
},
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @returns
*/
carDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
console.dir(formData);
const rawCar = formDataToObject(formData);
const apiURL = `${BASE_API_URI}/auth/cars`;
console.log('sending delete request to', JSON.stringify({ id: Number(rawCar.object.id) }));
/** @type {RequestInit} */
const requestOptions = {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify({ id: Number(rawCar.object.id) })
};
const res = await fetch(apiURL, requestOptions);
@@ -262,12 +303,9 @@ export const actions = {
let formData = await request.formData();
const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Types['subscription']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData);
/** @type {App.Locals['user']} */
const rawUser = /** @type {App.Locals['user']} */ (rawFormData.object);
const processedData = processUserFormData(rawUser);
console.dir(processedData);
const apiURL = `${BASE_API_URI}/auth/users/activate`;

View File

@@ -83,9 +83,7 @@
user.licence?.number?.toLowerCase()
].some((field) => field?.includes(term));
const subscriptionMatch = user.membership?.subscription_model?.name
?.toLowerCase()
.includes(term);
const subscriptionMatch = user.membership?.subscription?.name?.toLowerCase().includes(term);
const licenceCategoryMatch = user.licence?.categories?.some((cat) =>
cat.category.toLowerCase().includes(term)
@@ -147,7 +145,7 @@
on:click={() => setActiveSection('subscriptions')}
>
<i class="fas fa-clipboard-list"></i>
{$t('subscription.subscriptions')}
{$t('subscriptions.subscriptions')}
<span class="nav-badge">{subscriptions.length}</span>
</button>
</li>
@@ -277,8 +275,8 @@
<td>{user.email}</td>
</tr>
<tr>
<th>{$t('subscription.subscription')}</th>
<td>{user.membership?.subscription_model?.name}</td>
<th>{$t('subscriptions.subscription')}</th>
<td>{user.membership?.subscription?.name}</td>
</tr>
<tr>
<th>{$t('status')}</th>
@@ -393,47 +391,51 @@
</tbody>
</table>
<div class="button-group">
<button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
<form
method="POST"
action="?/userDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.user_deletion', {
values: {
firstname: user.first_name || '',
lastname: user.last_name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="user[id]" value={user.id} />
<input type="hidden" name="user[last_name]" value={user.last_name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
{#if hasPrivilige(user, PERMISSIONS.Update)}
<button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
</form>
{/if}
{#if hasPrivilige(user, PERMISSIONS.Delete)}
<form
method="POST"
action="?/userDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.user_deletion', {
values: {
firstname: user.first_name || '',
lastname: user.last_name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="user[id]" value={user.id} />
<input type="hidden" name="user[last_name]" value={user.last_name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
{/if}
</div>
</div>
</details>
@@ -441,7 +443,7 @@
</div>
{:else if activeSection === 'subscriptions'}
<div class="section-header">
<h2>{$t('subscription.subscriptions')}</h2>
<h2>{$t('subscriptions.subscriptions')}</h2>
{#if hasPrivilige(user, PERMISSIONS.Super)}
<button
class="btn primary"
@@ -462,7 +464,7 @@
<span class="nav-badge"
>{members.filter(
(/** @type{App.Locals['user']}*/ user) =>
user.membership?.subscription_model?.name === subscription.name
user.membership?.subscription?.name === subscription.name
).length}</span
>
</summary>
@@ -470,7 +472,7 @@
<table class="table">
<tbody>
<tr>
<th>{$t('subscription.monthly_fee')}</th>
<th>{$t('subscriptions.monthly_fee')}</th>
<td
>{subscription.monthly_fee !== -1
? subscription.monthly_fee + '€'
@@ -478,7 +480,7 @@
>
</tr>
<tr>
<th>{$t('subscription.hourly_rate')}</th>
<th>{$t('subscriptions.hourly_rate')}</th>
<td
>{subscription.hourly_rate !== -1
? subscription.hourly_rate + '€'
@@ -486,11 +488,11 @@
>
</tr>
<tr>
<th>{$t('subscription.included_hours_per_year')}</th>
<th>{$t('subscriptions.included_hours_per_year')}</th>
<td>{subscription.included_hours_per_year || 0}</td>
</tr>
<tr>
<th>{$t('subscription.included_hours_per_month')}</th>
<th>{$t('subscriptions.included_hours_per_month')}</th>
<td>{subscription.included_hours_per_month || 0}</td>
</tr>
<tr>
@@ -498,13 +500,13 @@
<td>{subscription.details || '-'}</td>
</tr>
<tr>
<th>{$t('subscription.conditions')}</th>
<th>{$t('subscriptions.conditions')}</th>
<td>{subscription.conditions || '-'}</td>
</tr>
</tbody>
</table>
{#if hasPrivilige(user, PERMISSIONS.Super)}
<div class="button-group">
<div class="button-group">
{#if hasPrivilige(user, PERMISSIONS.Super)}
<button
class="btn primary"
on:click={() => {
@@ -514,46 +516,46 @@
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
{#if !members.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription_model?.id === subscription.id)}
<form
method="POST"
action="?/subscriptionDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
} else {
document
.querySelector('.accordion-content')
?.scrollTo({ top: 0, behavior: 'smooth' });
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.subscription_deletion', {
values: {
name: subscription.name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
{/if}
{#if !members.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription?.id === subscription.id)}
<form
method="POST"
action="?/subscriptionDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
} else {
document
.querySelector('.accordion-content')
?.scrollTo({ top: 0, behavior: 'smooth' });
await applyAction(result);
}
}}
>
<input type="hidden" name="subscription[id]" value={subscription.id} />
<input type="hidden" name="subscription[name]" value={subscription.name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
{/if}
</div>
{/if}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.subscription_deletion', {
values: {
name: subscription.name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="subscription[id]" value={subscription.id} />
<input type="hidden" name="subscription[name]" value={subscription.name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
{/if}
</div>
</div>
</details>
{/each}
@@ -612,8 +614,8 @@
</tr>
</tbody>
</table>
{#if hasPrivilige(user, PERMISSIONS.Update)}
<div class="button-group">
<div class="button-group">
{#if hasPrivilige(user, PERMISSIONS.Update)}
<button
class="btn primary"
on:click={() => {
@@ -623,6 +625,8 @@
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
{/if}
{#if hasPrivilige(user, PERMISSIONS.Delete)}
<form
method="POST"
action="?/carDelete"
@@ -652,14 +656,14 @@
}
}}
>
<input type="hidden" name="subscription[id]" value={car.id} />
<input type="hidden" name="car[id]" value={car.id} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
</div>
{/if}
{/if}
</div>
</div>
</details>
{/each}
@@ -720,7 +724,7 @@
</Modal>
{:else if selected && 'brand' in selected}
<Modal on:close={close}>
<CarEditForm {form} editor={user} car={selected} on:cancel={close} on:close={close} />
<CarEditForm {form} editor={user} {users} car={selected} on:cancel={close} on:close={close} />
</Modal>
{/if}

View File

@@ -62,6 +62,16 @@ type SecurityConfig struct {
Burst int `json:"Burst" default:"60" envconfig:"BURST_LIMIT"`
} `json:"RateLimits"`
}
type CompanyConfig struct {
Name string `json:"Name" envconfig:"COMPANY_NAME"`
Address string `json:"Address" envconfig:"COMPANY_ADDRESS"`
City string `json:"City" envconfig:"COMPANY_CITY"`
ZipCode string `json:"ZipCode" envconfig:"COMPANY_ZIPCODE"`
Country string `json:"Country" envconfig:"COMPANY_COUNTRY"`
SepaPrefix string `json:"SepaPrefix" envconfig:"COMPANY_SEPA_PREFIX"`
}
type Config struct {
Auth AuthenticationConfig `json:"auth"`
Site SiteConfig `json:"site"`
@@ -72,6 +82,7 @@ type Config struct {
DB DatabaseConfig `json:"db"`
SMTP SMTPConfig `json:"smtp"`
Security SecurityConfig `json:"security"`
Company CompanyConfig `json:"company"`
}
var (
@@ -85,7 +96,9 @@ var (
Recipients RecipientsConfig
Env string
Security SecurityConfig
Company CompanyConfig
)
var environmentOptions map[string]bool = map[string]bool{
"development": true,
"production": true,
@@ -124,6 +137,7 @@ func LoadConfig() {
Security = CFG.Security
Env = CFG.Env
Site = CFG.Site
Company = CFG.Company
logger.Info.Printf("Config loaded: %#v", CFG)
}

View File

@@ -9,13 +9,13 @@ const (
DelayedPaymentStatus
SettledPaymentStatus
AwaitingPaymentStatus
MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailChangePasswordSubject = "Passwort Änderung angefordert"
MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang"
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
SupporterSubscriptionModelName = "Keins"
MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailChangePasswordSubject = "Passwort Änderung angefordert"
MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang"
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
SupporterSubscriptionName = "Keins"
)
var Licences = struct {

View File

@@ -89,9 +89,7 @@ func (cr *CarController) GetAll(c *gin.Context) {
func (cr *CarController) Delete(c *gin.Context) {
type input struct {
Car struct {
ID uint `json:"id" binding:"required,numeric"`
} `json:"car"`
ID uint `json:"id" binding:"required,numeric"`
}
var deleteData input
requestUser, err := cr.UserService.FromContext(c)
@@ -109,7 +107,7 @@ func (cr *CarController) Delete(c *gin.Context) {
utils.HandleValidationError(c, err)
return
}
err = cr.S.Delete(&deleteData.Car.ID)
err = cr.S.Delete(&deleteData.ID)
if err != nil {
utils.RespondWithError(c, err, "Error deleting car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
return

View File

@@ -100,7 +100,7 @@ func TestMain(t *testing.T) {
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{}
var subscriptionRepo repositories.SubscriptionModelsRepositoryInterface = &repositories.SubscriptionModelsRepository{}
var subscriptionRepo repositories.SubscriptionsRepositoryInterface = &repositories.SubscriptionsRepository{}
membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo}
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
@@ -119,6 +119,7 @@ func TestMain(t *testing.T) {
if err := initLicenceCategories(); err != nil {
log.Fatalf("Failed to init Categories: %v", err)
}
password := "securepassword"
admin := models.User{
FirstName: "Ad",
LastName: "min",
@@ -130,7 +131,7 @@ func TestMain(t *testing.T) {
ZipCode: "12345",
City: "SampleCity",
Status: constants.ActiveStatus,
Password: "",
Password: password,
Notes: "",
RoleID: constants.Roles.Admin,
Consents: nil,
@@ -140,7 +141,6 @@ func TestMain(t *testing.T) {
Licence: &models.Licence{
Status: constants.UnverifiedStatus,
}}
admin.SetPassword("securepassword")
admin.Create(db)
validation.SetupValidators(db)
t.Run("userController", func(t *testing.T) {
@@ -203,7 +203,7 @@ func initLicenceCategories() error {
}
func initSubscriptionPlans() error {
subscriptions := []models.SubscriptionModel{
subscriptions := []models.Subscription{
{
Name: "Basic",
Details: "Test Plan",
@@ -284,7 +284,7 @@ func getBaseUser() models.User {
City: "Hasloh",
Phone: "01738484993",
BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: &models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
Licence: nil,
Password: "passw@#$#%$!-ord123",
Company: "",
@@ -303,7 +303,7 @@ func getBaseSupporter() models.User {
City: "Hasloh",
Phone: "01738484993",
BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: &models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
Licence: nil,
Password: "passw@#$#%$!-ord123",
Company: "",

View File

@@ -33,7 +33,7 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
return
}
var subscription models.SubscriptionModel
var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err)
return
@@ -43,9 +43,9 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
id, err := mc.Service.RegisterSubscription(&subscription)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
utils.RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Duplicate)
utils.RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.Subscription, errors.Responses.Keys.Duplicate)
} else {
utils.RespondWithError(c, err, "Couldn't register Membershipmodel", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError)
utils.RespondWithError(c, err, "Couldn't register Membershipmodel", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
}
return
}
@@ -69,7 +69,7 @@ func (mc *MembershipController) UpdateHandler(c *gin.Context) {
return
}
var subscription models.SubscriptionModel
var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err)
return
@@ -122,7 +122,7 @@ func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
func (mc *MembershipController) GetSubscriptions(c *gin.Context) {
subscriptions, err := mc.Service.GetSubscriptions(nil)
if err != nil {
utils.RespondWithError(c, err, "Error retrieving subscriptions", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError)
utils.RespondWithError(c, err, "Error retrieving subscriptions", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
return
}

View File

@@ -148,8 +148,8 @@ func (dt *DeleteSubscriptionTest) ValidateResult() error {
return validateSubscription(dt.Assert, dt.WantDBData)
}
func getBaseSubscription() models.SubscriptionModel {
return models.SubscriptionModel{
func getBaseSubscription() models.Subscription {
return models.Subscription{
Name: "Premium",
Details: "A subscription detail",
MonthlyFee: 12.0,
@@ -157,7 +157,7 @@ func getBaseSubscription() models.SubscriptionModel {
}
}
func customizeSubscription(customize func(models.SubscriptionModel) models.SubscriptionModel) models.SubscriptionModel {
func customizeSubscription(customize func(models.Subscription) models.Subscription) models.Subscription {
subscription := getBaseSubscription()
return customize(subscription)
}
@@ -173,7 +173,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Just a Subscription"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = ""
return subscription
})),
@@ -187,7 +187,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": ""},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = ""
return subscription
})),
@@ -200,7 +200,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub models.SubscriptionModel) models.SubscriptionModel {
Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.MonthlyFee = -10.0
return sub
})),
@@ -213,7 +213,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub models.SubscriptionModel) models.SubscriptionModel {
Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.HourlyRate = -1.0
return sub
})),
@@ -227,7 +227,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1
@@ -243,7 +243,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1
@@ -274,7 +274,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "monthly_fee": "12"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.MonthlyFee = 123.0
return subscription
})),
@@ -288,7 +288,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.ID = 0
return subscription
})),
@@ -302,7 +302,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "hourly_rate": "14"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.HourlyRate = 3254.0
return subscription
})),
@@ -316,7 +316,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "included_per_year": "0"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.IncludedPerYear = 9873.0
return subscription
})),
@@ -330,7 +330,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "included_per_month": "1"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.IncludedPerMonth = 23415.0
return subscription
})),
@@ -344,7 +344,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "NonExistentSubscription"
return subscription
})),
@@ -358,7 +358,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "Altered Details"
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
@@ -375,7 +375,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "Altered Details"
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
@@ -388,7 +388,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
func getSubscriptionDeleteData() []DeleteSubscriptionTest {
var premiumSub, basicSub models.SubscriptionModel
var premiumSub, basicSub models.Subscription
database.DB.Where("name = ?", "Premium").First(&premiumSub)
database.DB.Where("name = ?", "Basic").First(&basicSub)
@@ -402,7 +402,7 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "NonExistentSubscription"
subscription.ID = basicSub.ID
logger.Error.Printf("subscription to delete: %#v", subscription)
@@ -418,7 +418,7 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": ""},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = ""
subscription.ID = basicSub.ID
return subscription
@@ -433,7 +433,7 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Basic"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Basic"
subscription.ID = basicSub.ID
return subscription
@@ -448,7 +448,7 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Premium"
subscription.ID = premiumSub.ID
return subscription
@@ -463,7 +463,7 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel {
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Premium"
subscription.ID = premiumSub.ID
return subscription

View File

@@ -132,9 +132,9 @@ func (uc *UserController) ChangePassword(c *gin.Context) {
utils.HandleValidationError(c, err)
return
}
if !user.Verify(input.Token, constants.VerificationTypes.Password) {
utils.RespondWithError(c, errors.ErrAlreadyVerified, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
err = user.Verify(input.Token, constants.VerificationTypes.Password)
if err != nil {
utils.RespondWithError(c, err, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}

View File

@@ -34,7 +34,6 @@ func setupTestContext() (*TestContext, error) {
logger.Error.Printf("error fetching user: %#v", err)
return nil, err
}
logger.Error.Printf("found user: %#v", user)
return &TestContext{
router: gin.Default(),
response: httptest.NewRecorder(),
@@ -104,7 +103,6 @@ func testChangePassword(t *testing.T, tc *TestContext) {
var verification models.Verification
result := database.DB.Where("user_id = ? AND type = ?", tc.user.ID, constants.VerificationTypes.Password).First(&verification)
assert.NoError(t, result.Error)
logger.Error.Printf("token from db: %#v", verification.VerificationToken)
requestBody := map[string]interface{}{
"password": "new-pas9247A@!sword",
"token": verification.VerificationToken,

View File

@@ -89,6 +89,10 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
var updateData RegistrationData
if err := c.ShouldBindJSON(&updateData); err != nil {
if updateData.User.Password != "" {
logger.Error.Printf("u.password: %#v", updateData.User.Password)
}
utils.HandleValidationError(c, err)
return
}
@@ -235,17 +239,19 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
var regData RegistrationData
if err := c.ShouldBindJSON(&regData); err != nil {
logger.Error.Printf("Failed initial Binding: %#v", &regData.User.Membership)
logger.Error.Printf("Failed initial Binding: %#v", &regData.User.Membership.Subscription)
utils.HandleValidationError(c, err)
return
}
logger.Info.Printf("Registering user %v", regData.User.Email)
selectedModel, err := uc.MembershipService.GetSubscriptionByName(&regData.User.Membership.SubscriptionModel.Name)
selectedModel, err := uc.MembershipService.GetSubscriptionByName(&regData.User.Membership.Subscription.Name)
if err != nil {
utils.RespondWithError(c, err, "Error in Registeruser, couldn't get selected model", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InvalidSubscriptionModel)
utils.RespondWithError(c, err, "Error in Registeruser, couldn't get selected model", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.InvalidSubscription)
return
}
regData.User.Membership.SubscriptionModel = *selectedModel
regData.User.Membership.Subscription = *selectedModel
// Get Gin's binding validator engine with all registered validators
validate := binding.Validator.Engine().(*validator.Validate)
@@ -254,7 +260,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
utils.HandleValidationError(c, err)
return
}
if regData.User.Membership.SubscriptionModel.Name == constants.SupporterSubscriptionModelName {
if regData.User.Membership.Subscription.Name == constants.SupporterSubscriptionName {
regData.User.RoleID = constants.Roles.Supporter
} else {
regData.User.RoleID = constants.Roles.Member
@@ -289,14 +295,14 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
LastName: regData.User.LastName,
Email: regData.User.Email,
ConsentType: "TermsOfService",
UserID: regData.User.ID,
UserID: &regData.User.ID,
},
{
FirstName: regData.User.FirstName,
LastName: regData.User.LastName,
Email: regData.User.Email,
ConsentType: "Privacy",
UserID: regData.User.ID,
UserID: &regData.User.ID,
},
}
@@ -347,7 +353,8 @@ func (uc *UserController) VerifyMailHandler(c *gin.Context) {
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user"})
return
}
if !user.Verify(token, constants.VerificationTypes.Email) {
err = user.Verify(token, constants.VerificationTypes.Email)
if err != nil {
logger.Error.Printf("Couldn't find user verification in verifyMailHandler: %v", err)
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user verification request"})
return

View File

@@ -351,8 +351,8 @@ func testCurrentUserHandler(t *testing.T, loginEmail string) http.Cookie {
if tt.expectedStatus == http.StatusOK {
var response struct {
User models.User `json:"user"`
Subscriptions []models.SubscriptionModel `json:"subscriptions"`
User models.User `json:"user"`
Subscriptions []models.Subscription `json:"subscriptions"`
}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
@@ -407,12 +407,15 @@ func validateUser(assert bool, wantDBData map[string]interface{}) error {
if assert {
user := (*users)[0]
// Check for mandate reference
if user.BankAccount.MandateReference == "" {
if user.BankAccount.IBAN != "" && user.BankAccount.MandateReference == "" {
return fmt.Errorf("Mandate reference not generated for user: %s", user.Email)
} else if user.BankAccount.IBAN == "" && user.BankAccount.MandateReference != "" {
return fmt.Errorf("Mandate reference generated without IBAN for user: %s", user.Email)
}
// Validate mandate reference format
expected := user.GenerateMandateReference()
expected := user.BankAccount.GenerateMandateReference(user.ID)
if !strings.HasPrefix(user.BankAccount.MandateReference, expected) {
return fmt.Errorf("Mandate reference is invalid. Expected: %s, Got: %s", expected, user.BankAccount.MandateReference)
}
@@ -686,6 +689,20 @@ func testUpdateUser(t *testing.T) {
},
expectedStatus: http.StatusAccepted,
},
{
name: "Admin Password Update low entropy should fail",
setupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = "newpassword"
},
expectedErrors: []map[string]string{
{"field": "server.validation.special server.validation.uppercase server.validation.numbers server.validation.longer", "key": "server.validation.insecure"},
},
expectedStatus: http.StatusBadRequest,
},
{
name: "Admin Password Update",
setupCookie: func(req *http.Request) {
@@ -792,7 +809,11 @@ func testUpdateUser(t *testing.T) {
if updatedUser.Password == "" {
assert.Equal(t, user.Password, (*updatedUserFromDB).Password)
} else {
assert.NotEqual(t, user.Password, (*updatedUserFromDB).Password)
matches, err := updatedUserFromDB.PasswordMatches(updatedUser.Password)
if err != nil {
t.Fatalf("Error matching password: %v", err)
}
assert.True(t, matches, "Password mismatch")
}
updatedUserFromDB.Password = ""
@@ -820,7 +841,7 @@ func testUpdateUser(t *testing.T) {
assert.Equal(t, updatedUser.Membership.StartDate, updatedUserFromDB.Membership.StartDate, "Membership.StartDate mismatch")
assert.Equal(t, updatedUser.Membership.EndDate, updatedUserFromDB.Membership.EndDate, "Membership.EndDate mismatch")
assert.Equal(t, updatedUser.Membership.Status, updatedUserFromDB.Membership.Status, "Membership.Status mismatch")
assert.Equal(t, updatedUser.Membership.SubscriptionModelID, updatedUserFromDB.Membership.SubscriptionModelID, "Membership.SubscriptionModelID mismatch")
assert.Equal(t, updatedUser.Membership.SubscriptionID, updatedUserFromDB.Membership.SubscriptionID, "Membership.SubscriptionID mismatch")
assert.Equal(t, updatedUser.Membership.ParentMembershipID, updatedUserFromDB.Membership.ParentMembershipID, "Membership.ParentMembershipID mismatch")
if updatedUser.Licence == nil {
@@ -871,11 +892,11 @@ func checkWelcomeMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, user.FirstName) {
return fmt.Errorf("User first name(%v) has not been rendered in registration mail.", user.FirstName)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.MonthlyFee)
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.Subscription.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.MonthlyFee)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.HourlyRate)
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.Subscription.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.HourlyRate)
}
if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
@@ -907,11 +928,11 @@ func checkRegistrationMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, user.FirstName+" "+user.LastName) {
return fmt.Errorf("User first and last name(%v) has not been rendered in registration mail.", user.FirstName+" "+user.LastName)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.MonthlyFee)
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.Subscription.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.MonthlyFee)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.HourlyRate)
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.Subscription.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.HourlyRate)
}
if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
@@ -951,7 +972,7 @@ func checkVerificationMail(message *utils.Email, user *models.User) error {
if err != nil {
return fmt.Errorf("Error parsing verification URL: %#v", err.Error())
}
v, err := user.GetVerification(constants.VerificationTypes.Email)
v, err := user.FindVerification(constants.VerificationTypes.Email)
if err != nil {
return fmt.Errorf("Error getting verification token: %v", err.Error())
}
@@ -1132,7 +1153,7 @@ func getTestUsers() []RegisterUserTest {
user.BankAccount.IBAN = "DE1234234123134"
user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com"
user.Membership.SubscriptionModel.Name = constants.SupporterSubscriptionModelName
user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user
})),
},
@@ -1145,7 +1166,7 @@ func getTestUsers() []RegisterUserTest {
user.BankAccount.IBAN = ""
user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com"
user.Membership.SubscriptionModel.Name = constants.SupporterSubscriptionModelName
user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user
})),
},
@@ -1155,7 +1176,7 @@ func getTestUsers() []RegisterUserTest {
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = ""
user.Membership.Subscription.Name = ""
return user
})),
},
@@ -1165,7 +1186,7 @@ func getTestUsers() []RegisterUserTest {
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "NOTEXISTENTPLAN"
user.Membership.Subscription.Name = "NOTEXISTENTPLAN"
return user
})),
},
@@ -1204,7 +1225,7 @@ func getTestUsers() []RegisterUserTest {
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.SubscriptionModel.Name = "additional"
user.Membership.Subscription.Name = "additional"
return user
})),
},
@@ -1216,7 +1237,7 @@ func getTestUsers() []RegisterUserTest {
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 200
user.Membership.SubscriptionModel.Name = "additional"
user.Membership.Subscription.Name = "additional"
return user
})),
},
@@ -1228,7 +1249,7 @@ func getTestUsers() []RegisterUserTest {
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 1
user.Membership.SubscriptionModel.Name = "additional"
user.Membership.Subscription.Name = "additional"
return user
})),
},

View File

@@ -10,7 +10,6 @@ import (
"fmt"
"time"
"github.com/alexedwards/argon2id"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
@@ -51,9 +50,9 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
sqlDB.SetMaxIdleConns(1)
sqlDB.SetConnMaxLifetime(time.Hour)
db.Exec("PRAGMA foreign_keys = OFF;")
if err := db.AutoMigrate(
&models.User{},
&models.SubscriptionModel{},
&models.Subscription{},
&models.Membership{},
&models.Consent{},
&models.Verification{},
@@ -63,11 +62,12 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
&models.Car{},
&models.Location{},
&models.Damage{},
&models.Insurance{},
// &models.Insurance{},
// &models.User{},
); err != nil {
return nil, fmt.Errorf("failed to migrate database: %w", err)
}
db.Exec("PRAGMA foreign_keys = ON;")
logger.Info.Print("Opened DB")
DB = db
var categoriesCount int64
@@ -83,12 +83,12 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
}
var subscriptionsCount int64
db.Model(&models.SubscriptionModel{}).Count(&subscriptionsCount)
subscriptionModels := createSubscriptionModels()
for _, model := range subscriptionModels {
db.Model(&models.Subscription{}).Count(&subscriptionsCount)
subscriptions := createSubscriptions()
for _, model := range subscriptions {
var exists int64
db.
Model(&models.SubscriptionModel{}).
Model(&models.Subscription{}).
Where("name = ?", model.Name).
Count(&exists)
logger.Error.Printf("looked for model.name %v and found %v", model.Name, exists)
@@ -103,7 +103,7 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
var userCount int64
db.Model(&models.User{}).Count(&userCount)
if userCount == 0 {
var createdModel models.SubscriptionModel
var createdModel models.Subscription
if err := db.First(&createdModel).Error; err != nil {
return nil, err
}
@@ -118,10 +118,10 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
return db, nil
}
func createSubscriptionModels() []models.SubscriptionModel {
return []models.SubscriptionModel{
func createSubscriptions() []models.Subscription {
return []models.Subscription{
{
Name: constants.SupporterSubscriptionModelName,
Name: constants.SupporterSubscriptionName,
Details: "Dieses Modell ist für Sponsoren und Nichtmitglieder, die keinen Vereinsmitglied sind.",
HourlyRate: 999,
MonthlyFee: 0,
@@ -162,11 +162,6 @@ func createAdmin(userMail string) (*models.User, error) {
// Encode into a URL-safe base64 string
password := base64.URLEncoding.EncodeToString(passwordBytes)[:12]
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
return nil, err
}
logger.Error.Print("==============================================================")
logger.Error.Printf("Admin Email: %v", userMail)
logger.Error.Printf("Admin Password: %v", password)
@@ -176,7 +171,7 @@ func createAdmin(userMail string) (*models.User, error) {
FirstName: "Ad",
LastName: "Min",
DateOfBirth: time.Now().AddDate(-20, 0, 0),
Password: hash,
Password: password,
Company: "",
Address: "",
ZipCode: "",

View File

@@ -1,13 +1,54 @@
package models
import "time"
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Insurance struct {
ID uint `gorm:"primary_key" json:"id"`
OwnerID uint `gorm:"not null" json:"owner_id" binding:"numeric"`
Cars []Car `gorm:"many2many:car_insurances;" json:"-"`
Company string `json:"company" binding:"safe_content"`
Reference string `json:"reference" binding:"safe_content"`
Notes string `json:"notes" binding:"safe_content"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
}
func (i *Insurance) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(i).Error; err != nil {
return err
}
logger.Info.Printf("Insurance created: %#v", i)
// Preload all associations to return the fully populated User
return tx.
First(i, i.ID).Error // Refresh the user object with all associations
})
}
func (i *Insurance) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingInsurance Insurance
logger.Info.Printf("updating Insurance: %#v", i)
if err := tx.First(&existingInsurance, i.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingInsurance).Updates(i).Error; err != nil {
return err
}
return tx.First(i, i.ID).Error
})
}
func (i *Insurance) Delete(db *gorm.DB) error {
return db.Delete(&i).Error
}

View File

@@ -1,7 +1,9 @@
package models
import (
"GoMembership/internal/config"
"GoMembership/pkg/logger"
"fmt"
"time"
"gorm.io/gorm"
@@ -11,6 +13,7 @@ type BankAccount struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-" binding:"-"`
UserID uint `gorm:"index" json:"user_id"`
MandateDateSigned time.Time `json:"mandate_date_signed"`
Bank string `json:"bank_name" binding:"safe_content"`
@@ -48,3 +51,10 @@ func (b *BankAccount) Update(db *gorm.DB) error {
func (b *BankAccount) Delete(db *gorm.DB) error {
return db.Delete(&b).Error
}
func (b *BankAccount) GenerateMandateReference(id uint) string {
if b.IBAN == "" {
return ""
}
return fmt.Sprintf("%s-%s%d-%s", config.Company.SepaPrefix, time.Now().Format("20060102"), id, b.IBAN[len(b.IBAN)-4:])
}

View File

@@ -1,123 +1,151 @@
package models
import (
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Car struct {
ID uint `gorm:"primarykey" json:"id"`
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Status uint `json:"status"`
Name string `json:"name"`
Brand string `gorm:"not null" json:"brand"`
Model string `gorm:"not null" json:"model"`
Color string `gorm:"not null" json:"color"`
LicencePlate string `gorm:"not null,unique" json:"licence_plate"`
Price float32 `json:"price"`
Rate float32 `json:"rate"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Location Location `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"location"`
LocationID uint
Damages *[]Damage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"damages"`
Insurances *[]Insurance `gorm:"foreignkey:OwnerID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"insurance"`
Notes string `json:"notes"`
}
type Location struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
}
type Damage struct {
ID uint `gorm:"primarykey" json:"id"`
CarID uint `json:"car_id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Opponent *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"opponent"`
OpponentID uint
Insurance *Insurance `gorm:"foreignkey:OwnerID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"insurance"`
InsuranceID uint
Notes string `json:"notes"`
Status uint `json:"status"`
Name string `json:"name"`
Brand string `gorm:"not null" json:"brand"`
Model string `gorm:"not null" json:"model"`
Color string `gorm:"not null" json:"color"`
LicencePlate string `gorm:"not null,unique" json:"licence_plate"`
Price float32 `gorm:"type:decimal(10,2)" json:"price"`
Rate float32 `gorm:"type:decimal(10,2)" json:"rate"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Location *Location `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"location"`
Damages []Damage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"damages"`
Insurances []Insurance `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;many2many:car_insurances" json:"insurances"`
Notes string `json:"notes"`
}
func (c *Car) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(c).Error; err != nil {
if err := tx.Preload(clause.Associations).Create(c).Error; err != nil {
return err
}
// Replace associated Categories (assumes Categories already exist)
if c.Insurances != nil {
if err := tx.Model(c).Association("Insurances").Replace(c.Insurances); err != nil {
return err
}
}
logger.Info.Printf("car created: %#v", c)
// Preload all associations to return the fully populated User
return tx.
Preload("Insurances").
First(c, c.ID).Error // Refresh the user object with all associations
Preload(clause.Associations).
First(c, c.ID).Error
})
}
func (c *Car) Update(db *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingCar Car
logger.Info.Printf("updating car: %#v", c)
if err := tx.
Preload("Insurances").
if err := tx.Preload("Damages.Insurance").
Preload("Damages.Opponent").
First(&existingCar, c.ID).Error; err != nil {
return err
}
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(c)
if result.Error != nil {
logger.Error.Printf("car update error: %#v", result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.ErrNoRowsAffected
}
if c.Insurances != nil {
if err := tx.Save(*c.Insurances).Error; err != nil {
return err
}
if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(c).Error; err != nil {
return err
}
return nil
// if err := tx.Model(c).Association("Damages").Replace(c.Damages); err != nil {
// return err
// }
// if err := tx.Model(c).Association("Insurances").Replace(c.Insurances); err != nil {
// return err
// }
// Calculate damage IDs to delete
// existingDamageIDs := make(map[uint]bool)
// for _, d := range existingCar.Damages {
// existingDamageIDs[d.ID] = true
// }
// newDamageIDs := make(map[uint]bool)
// for _, d := range c.Damages {
// if d.ID != 0 {
// newDamageIDs[d.ID] = true
// }
// }
// // Find IDs to delete
// var toDelete []uint
// for id := range existingDamageIDs {
// if !newDamageIDs[id] {
// toDelete = append(toDelete, id)
// }
// }
// // Batch delete orphaned damages
// if len(toDelete) > 0 {
// if err := tx.Where("id IN ?", toDelete).Delete(&Damage{}).Error; err != nil {
// return err
// }
// }
// if len(c.Insurances) > 0 {
// logger.Info.Printf("updating insurances: %#v", c.Insurances)
// if err := tx.Model(&existingCar).Association("Insurances").Replace(c.Insurances); err != nil {
// return err
// }
// }
// // Upsert new damages
// for _, damage := range c.Damages {
// // Process relationships
// if damage.Opponent != nil {
// if err := tx.Save(damage.Opponent).Error; err != nil {
// return err
// }
// damage.OpponentID = damage.Opponent.ID
// }
// if damage.Insurance != nil {
// if err := tx.Save(damage.Insurance).Error; err != nil {
// return err
// }
// damage.InsuranceID = damage.Insurance.ID
// }
// // Create or update damage
// if err := tx.Save(damage).Error; err != nil {
// return err
// }
// }
// // Update associations
// if err := tx.Model(&existingCar).Association("Damages").Replace(c.Damages); err != nil {
// return err
// }
return tx.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
First(c, c.ID).Error
})
if err != nil {
return err
}
return db.
Preload("Insurances").
First(&c, c.ID).Error
}
func (c *Car) Delete(db *gorm.DB) error {
return db.Delete(&c).Error
return db.Select(clause.Associations).Delete(&c).Error
}
func GetAllCars(db *gorm.DB) ([]Car, error) {
var cars []Car
if err := db.Find(&cars).Error; err != nil {
if err := db.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
Find(&cars).Error; err != nil {
return nil, err
}
return cars, nil
@@ -125,7 +153,11 @@ func GetAllCars(db *gorm.DB) ([]Car, error) {
func (c *Car) FromID(db *gorm.DB, id uint) error {
var car Car
if err := db.Preload("Insurances").First(&car, id).Error; err != nil {
if err := db.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
First(&car, id).Error; err != nil {
return err
}
*c = car

View File

@@ -16,7 +16,8 @@ type Consent struct {
LastName string `gorm:"not null" json:"last_name" binding:"safe_content"`
Email string `json:"email" binding:"email,safe_content"`
ConsentType string `gorm:"not null" json:"consent_type" binding:"safe_content"`
UserID uint `gorm:"not null" json:"user_id"`
UserID *uint `json:"user_id"`
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-" binding:"-"`
}
func (c *Consent) BeforeSave(tx *gorm.DB) (err error) {

View File

@@ -0,0 +1,58 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Damage struct {
ID uint `gorm:"primaryKey" json:"id"`
CarID uint `json:"car_id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Name string `json:"name"`
Date time.Time `json:"date"`
Opponent *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"opponent"`
OpponentID uint `json:"opponent_id"`
Driver *User `json:"driver"`
DriverID uint `json:"driver_id"`
Insurance *Insurance `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"insurance"`
InsuranceID uint `json:"insurance_id"`
Notes string `json:"notes"`
}
func (d *Damage) Create(db *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := db.Create(d).Error; err != nil {
return err
}
logger.Info.Printf("Damage created: %#v", d)
// Preload all associations to return the fully populated User
return db.First(d, d.ID).Error // Refresh the user object with all associations
}
func (d *Damage) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingDamage Damage
logger.Info.Printf("updating Damage: %#v", d)
if err := tx.First(&existingDamage, d.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingDamage).Updates(d).Error; err != nil {
return err
}
return tx.First(d, d.ID).Error
})
}
func (d *Damage) Delete(db *gorm.DB) error {
return db.Delete(&d).Error
}

View File

@@ -0,0 +1,54 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Location struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
CarID uint `gorm:"index" json:"car_id"`
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
}
func (l *Location) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(l).Error; err != nil {
return err
}
logger.Info.Printf("Location created: %#v", l)
// Preload all associations to return the fully populated User
return tx.
First(l, l.ID).Error // Refresh the user object with all associations
})
}
func (l *Location) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingLocation Location
logger.Info.Printf("updating Location: %#v", l)
if err := tx.First(&existingLocation, l.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingLocation).Updates(l).Error; err != nil {
return err
}
return tx.First(l, l.ID).Error
})
}
func (l *Location) Delete(db *gorm.DB) error {
return db.Delete(&l).Error
}

View File

@@ -8,20 +8,21 @@ import (
)
type Membership struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index" json:"user_id"`
CreatedAt time.Time
UpdatedAt time.Time
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Status int8 `json:"status" binding:"number,safe_content"`
SubscriptionModel SubscriptionModel `gorm:"foreignKey:SubscriptionModelID" json:"subscription_model"`
SubscriptionModelID uint `json:"subsription_model_id"`
ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"`
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index" json:"user_id"`
User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-" binding:"-"`
CreatedAt time.Time
UpdatedAt time.Time
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Status int8 `json:"status" binding:"number,safe_content"`
Subscription Subscription `gorm:"foreignKey:SubscriptionID" json:"subscription"`
SubscriptionID uint `json:"subscription_id"`
ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"`
}
func (m *Membership) BeforeSave(tx *gorm.DB) error {
m.SubscriptionModelID = m.SubscriptionModel.ID
m.SubscriptionID = m.Subscription.ID
return nil
}
@@ -31,7 +32,7 @@ func (m *Membership) Create(db *gorm.DB) error {
}
logger.Info.Printf("Membership created: %#v", m)
return db.Preload("SubscriptionModel").First(m, m.ID).Error // Refresh the user object with SubscriptionModel
return db.Preload("Subscription").First(m, m.ID).Error // Refresh the user object with Subscription
}
func (m *Membership) Update(db *gorm.DB) error {

View File

@@ -7,7 +7,7 @@ import (
"gorm.io/gorm"
)
type SubscriptionModel struct {
type Subscription struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
@@ -21,31 +21,31 @@ type SubscriptionModel struct {
IncludedPerMonth int16 `json:"included_hours_per_month"`
}
func (s *SubscriptionModel) Create(db *gorm.DB) error {
func (s *Subscription) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(s).Error; err != nil {
return err
}
logger.Info.Printf("SubscriptionModel created: %#v", s)
logger.Info.Printf("Subscription created: %#v", s)
// Preload all associations to retuvn the fully populated User
return tx.
First(s, s.ID).Error // Refresh the user object with all associations
})
}
func (s *SubscriptionModel) Update(db *gorm.DB) error {
func (s *Subscription) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingSubscriptionModel SubscriptionModel
var existingSubscription Subscription
logger.Info.Printf("updating SubscriptionModel: %#v", s)
if err := tx.First(&existingSubscriptionModel, s.ID).Error; err != nil {
logger.Info.Printf("updating Subscription: %#v", s)
if err := tx.First(&existingSubscription, s.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingSubscriptionModel).Updates(s).Error; err != nil {
if err := tx.Model(&existingSubscription).Updates(s).Error; err != nil {
return err
}
return tx.First(s, s.ID).Error
@@ -53,6 +53,6 @@ func (s *SubscriptionModel) Update(db *gorm.DB) error {
}
func (s *SubscriptionModel) Delete(db *gorm.DB) error {
func (s *Subscription) Delete(db *gorm.DB) error {
return db.Delete(&s).Error
}

View File

@@ -3,7 +3,6 @@ package models
import (
"GoMembership/internal/config"
"GoMembership/internal/constants"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"fmt"
@@ -34,7 +33,7 @@ type User struct {
Address string `gorm:"not null" json:"address" binding:"required,safe_content"`
ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"`
City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"`
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"`
Consents []Consent `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
BankAccount *BankAccount `gorm:"foreignkey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"bank_account"`
Verifications []Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Membership *Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"membership"`
@@ -45,7 +44,7 @@ type User struct {
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.BankAccount != nil && u.BankAccount.MandateReference == "" {
u.BankAccount.MandateReference = u.GenerateMandateReference()
u.BankAccount.MandateReference = u.BankAccount.GenerateMandateReference(u.ID)
u.BankAccount.Update(tx)
}
return nil
@@ -53,38 +52,16 @@ func (u *User) AfterCreate(tx *gorm.DB) (err error) {
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
u.Email = strings.ToLower(u.Email)
return nil
}
func (u *User) GenerateMandateReference() string {
return fmt.Sprintf("%s%d%s", time.Now().Format("20060102"), u.ID, u.BankAccount.IBAN)
}
func (u *User) SetPassword(plaintextPassword string) error {
if plaintextPassword == "" {
return nil
if u.Password != "" {
hash, err := argon2id.CreateHash(u.Password, argon2id.DefaultParams)
if err != nil {
return err
}
u.Password = hash
}
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil {
return err
}
u.Password = hash
return nil
}
func (u *User) PasswordMatches(plaintextPassword string) (bool, error) {
return argon2id.ComparePasswordAndHash(plaintextPassword, u.Password)
}
func (u *User) PasswordExists() bool {
return u.Password != ""
}
func (u *User) Delete(db *gorm.DB) error {
return db.Delete(&User{}, "id = ?", u.ID).Error
}
func (u *User) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
@@ -96,118 +73,6 @@ func (u *User) Create(db *gorm.DB) error {
})
}
// return db.Transaction(func(tx *gorm.DB) error {
// // Initialize slices/pointers if nil
// if u.Verifications == nil {
// u.Verifications = &[]Verification{}
// }
// // Create base user first
// if err := tx.Omit(clause.Associations).Create(u).Error; err != nil {
// return fmt.Errorf("failed to create user: %w", err)
// }
// // Handle BankAccount
// if u.BankAccount != (BankAccount{}) {
// u.BankAccount.MandateReference = u.GenerateMandateReference()
// if err := tx.Create(&u.BankAccount).Error; err != nil {
// return fmt.Errorf("failed to create bank account: %w", err)
// }
// if err := tx.Model(u).Update("bank_account_id", u.BankAccount.ID).Error; err != nil {
// return fmt.Errorf("failed to link bank account: %w", err)
// }
// }
// // Handle Membership and SubscriptionModel
// if u.Membership != (Membership{}) {
// if err := tx.Create(&u.Membership).Error; err != nil {
// return fmt.Errorf("failed to create membership: %w", err)
// }
// if err := tx.Model(u).Update("membership_id", u.Membership.ID).Error; err != nil {
// return fmt.Errorf("failed to link membership: %w", err)
// }
// }
// // Handle Licence and Categories
// if u.Licence != nil {
// u.Licence.UserID = u.ID
// if err := tx.Create(u.Licence).Error; err != nil {
// return fmt.Errorf("failed to create licence: %w", err)
// }
// if len(u.Licence.Categories) > 0 {
// if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
// return fmt.Errorf("failed to link categories: %w", err)
// }
// }
// if err := tx.Model(u).Update("licence_id", u.Licence.ID).Error; err != nil {
// return fmt.Errorf("failed to link licence: %w", err)
// }
// }
// // Handle Consents
// for i := range u.Consents {
// u.Consents[i].UserID = u.ID
// }
// if len(u.Consents) > 0 {
// if err := tx.Create(&u.Consents).Error; err != nil {
// return fmt.Errorf("failed to create consents: %w", err)
// }
// }
// // Handle Verifications
// for i := range *u.Verifications {
// (*u.Verifications)[i].UserID = u.ID
// }
// if len(*u.Verifications) > 0 {
// if err := tx.Create(u.Verifications).Error; err != nil {
// return fmt.Errorf("failed to create verifications: %w", err)
// }
// }
// // Reload the complete user with all associations
// return tx.Preload(clause.Associations).
// Preload("Membership.SubscriptionModel").
// Preload("Licence.Categories").
// First(u, u.ID).Error
// })
// }
// func (u *User) Create(db *gorm.DB) error {
// return db.Transaction(func(tx *gorm.DB) error {
// // Create the base User record (omit associations to handle them separately)
// if err := tx.Create(u).Error; err != nil {
// return err
// }
// for i := range u.Consents {
// u.Consents[i].UserID = u.ID
// }
// for i := range *u.Verifications {
// (*u.Verifications)[i].UserID = u.ID
// }
// if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(u).Error; err != nil {
// return err
// }
// // Replace associated Categories (assumes Categories already exist)
// if u.Licence != nil && len(u.Licence.Categories) > 0 {
// if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
// return err
// }
// }
// logger.Info.Printf("user created: %#v", u.Safe())
// // Preload all associations to return the fully populated User
// return tx.
// Preload(clause.Associations).
// Preload("Membership.SubscriptionModel").
// Preload("Licence.Categories").
// First(u, u.ID).Error // Refresh the user object with all associations
// })
// }
func (u *User) Update(db *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error {
@@ -220,7 +85,7 @@ func (u *User) Update(db *gorm.DB) error {
return err
}
// Update the user's main fields
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Password", "Verifications", "Licence.Categories").Updates(u)
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Verifications", "Licence.Categories").Updates(u)
if result.Error != nil {
logger.Error.Printf("User update error in update user: %#v", result.Error)
return result.Error
@@ -229,57 +94,6 @@ func (u *User) Update(db *gorm.DB) error {
return errors.ErrNoRowsAffected
}
if u.Password != "" {
if err := tx.Model(&existingUser).
Update("Password", u.Password).Error; err != nil {
logger.Error.Printf("Password update error in update user: %#v", err)
return err
}
}
// // Update the Membership if provided
// if u.Membership.ID != 0 {
// if err := tx.Model(&existingUser.Membership).Updates(u.Membership).Where("id = ?", existingUser.Membership.ID).Error; err != nil {
// logger.Error.Printf("Membership update error in update user: %#v", err)
// return err
// }
// }
// if u.Licence != nil {
// u.Licence.UserID = existingUser.ID
// if err := tx.Save(u.Licence).Error; err != nil {
// return err
// }
// if err := tx.Model(&existingUser).Update("LicenceID", u.Licence.ID).Error; err != nil {
// return err
// }
// if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
// return err
// }
// }
// if u.Licence != nil {
// if existingUser.Licence == nil || existingUser.LicenceID == 0 {
// u.Licence.UserID = existingUser.ID // Ensure Licence belongs to User
// if err := tx.Create(u.Licence).Error; err != nil {
// return err
// }
// existingUser.Licence = u.Licence
// existingUser.LicenceID = u.Licence.ID
// if err := tx.Model(&existingUser).Update("LicenceID", u.Licence.ID).Error; err != nil {
// return err
// }
// }
// if err := tx.Model(existingUser.Licence).Updates(u.Licence).Error; err != nil {
// return err
// }
// // Update Categories association
// if err := tx.Model(existingUser.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
// return err
// }
// }
if u.Verifications != nil {
if err := tx.Save(u.Verifications).Error; err != nil {
return err
@@ -300,16 +114,21 @@ func (u *User) Update(db *gorm.DB) error {
return db.
Preload(clause.Associations).
Preload("Membership.SubscriptionModel").
Preload("Membership.Subscription").
Preload("Licence.Categories").
First(&u, u.ID).Error
}
func (u *User) Delete(db *gorm.DB) error {
return db.Unscoped().Delete(&User{}, "id = ?", u.ID).Error
// return db.Delete(&User{}, "id = ?", u.ID).Error
}
func (u *User) FromID(db *gorm.DB, userID *uint) error {
var user User
result := db.
Preload(clause.Associations).
Preload("Membership.SubscriptionModel").
Preload("Membership.Subscription").
Preload("Licence.Categories").
First(&user, userID)
if result.Error != nil {
@@ -326,7 +145,7 @@ func (u *User) FromEmail(db *gorm.DB, email *string) error {
var user User
result := db.
Preload(clause.Associations).
Preload("Membership.SubscriptionModel").
Preload("Membership.Subscription").
Preload("Licence.Categories").
Where("email = ?", email).First(&user)
if result.Error != nil {
@@ -354,6 +173,14 @@ func (u *User) FromContext(db *gorm.DB, c *gin.Context) error {
return nil
}
func (u *User) PasswordMatches(plaintextPassword string) (bool, error) {
return argon2id.ComparePasswordAndHash(plaintextPassword, u.Password)
}
func (u *User) PasswordExists() bool {
return u.Password != ""
}
func (u *User) IsVerified() bool {
return u.Status > constants.DisabledStatus
}
@@ -378,24 +205,20 @@ func (u *User) SetVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil {
u.Verifications = []Verification{}
}
token, err := utils.GenerateVerificationToken()
v, err := CreateVerification(verificationType)
if err != nil {
return nil, err
}
v := Verification{
UserID: u.ID,
VerificationToken: token,
Type: verificationType,
}
v.UserID = u.ID
if vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 {
u.Verifications[vi] = v
u.Verifications[vi] = *v
} else {
u.Verifications = append(u.Verifications, v)
u.Verifications = append(u.Verifications, *v)
}
return &v, nil
return v, nil
}
func (u *User) GetVerification(verificationType string) (*Verification, error) {
func (u *User) FindVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil {
return nil, errors.ErrNoData
}
@@ -406,10 +229,10 @@ func (u *User) GetVerification(verificationType string) (*Verification, error) {
return &u.Verifications[vi], nil
}
func (u *User) Verify(token string, verificationType string) bool {
func (u *User) Verify(token string, verificationType string) error {
if token == "" || verificationType == "" {
logger.Error.Printf("token or verification type are empty in user.Verify")
return false
return errors.ErrNoData
}
vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool {
@@ -418,17 +241,9 @@ func (u *User) Verify(token string, verificationType string) bool {
if vi == -1 {
logger.Error.Printf("Couldn't find verification in users verifications")
return false
return errors.ErrNotFound
}
if u.Verifications[vi].VerifiedAt != nil {
logger.Error.Printf("VerifiedAt is not nil, already verified?: %#v", u.Verifications[vi])
return false
}
t := time.Now()
u.Verifications[vi].VerifiedAt = &t
return true
return u.Verifications[vi].Validate()
}
func (u *User) Safe() map[string]interface{} {
@@ -441,15 +256,15 @@ func (u *User) Safe() map[string]interface{} {
"start_date": u.Membership.StartDate,
"end_date": u.Membership.EndDate,
"status": u.Membership.Status,
"subscription_model": map[string]interface{}{
"id": u.Membership.SubscriptionModel.ID,
"name": u.Membership.SubscriptionModel.Name,
"details": u.Membership.SubscriptionModel.Details,
"conditions": u.Membership.SubscriptionModel.Conditions,
"monthly_fee": u.Membership.SubscriptionModel.MonthlyFee,
"hourly_rate": u.Membership.SubscriptionModel.HourlyRate,
"included_per_year": u.Membership.SubscriptionModel.IncludedPerYear,
"included_per_month": u.Membership.SubscriptionModel.IncludedPerMonth,
"subscription": map[string]interface{}{
"id": u.Membership.Subscription.ID,
"name": u.Membership.Subscription.Name,
"details": u.Membership.Subscription.Details,
"conditions": u.Membership.Subscription.Conditions,
"monthly_fee": u.Membership.Subscription.MonthlyFee,
"hourly_rate": u.Membership.Subscription.HourlyRate,
"included_per_year": u.Membership.Subscription.IncludedPerYear,
"included_per_month": u.Membership.Subscription.IncludedPerMonth,
},
}
}
@@ -543,7 +358,7 @@ func GetUsersWhere(db *gorm.DB, where map[string]interface{}) (*[]User, error) {
var users []User
result := db.
Preload(clause.Associations).
Preload("Membership.SubscriptionModel").
Preload("Membership.Subscription").
Preload("Licence.Categories").
Where(where).Find(&users)
if result.Error != nil {

View File

@@ -1,6 +1,8 @@
package models
import (
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"time"
@@ -46,3 +48,31 @@ func (v *Verification) Update(db *gorm.DB) error {
func (v *Verification) Delete(db *gorm.DB) error {
return db.Delete(&v).Error
}
func (v *Verification) Validate() error {
if v.VerifiedAt != nil {
return errors.ErrAlreadyVerified
}
t := time.Now()
v.VerifiedAt = &t
return nil
}
func CreateVerification(verificationType string) (*Verification, error) {
token, err := GenerateVerificationToken()
if err != nil {
return nil, err
}
v := Verification{
UserID: 0,
VerificationToken: token,
Type: verificationType,
}
return &v, nil
}
func GenerateVerificationToken() (string, error) {
return utils.GenerateRandomString(32)
}

View File

@@ -1,97 +0,0 @@
package repositories
import (
"GoMembership/internal/database"
"gorm.io/gorm"
"GoMembership/internal/models"
)
type SubscriptionModelsRepositoryInterface interface {
CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (uint, error)
UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error)
GetSubscriptionModelNames() ([]string, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
// GetUsersBySubscription(id uint) (*[]models.SubscriptionModel, error)
DeleteSubscription(id *uint) error
}
type SubscriptionModelsRepository struct{}
func (sr *SubscriptionModelsRepository) CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (uint, error) {
result := database.DB.Create(subscriptionModel)
if result.Error != nil {
return 0, result.Error
}
return subscriptionModel.ID, nil
}
func (sr *SubscriptionModelsRepository) UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error) {
result := database.DB.Model(&models.SubscriptionModel{ID: subscription.ID}).Updates(subscription)
if result.Error != nil {
return nil, result.Error
}
return subscription, nil
}
func (sr *SubscriptionModelsRepository) DeleteSubscription(id *uint) error {
result := database.DB.Delete(&models.SubscriptionModel{}, id)
if result.Error != nil {
return result.Error
}
return nil
}
func GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error) {
var model models.SubscriptionModel
result := database.DB.Where("name = ?", modelname).First(&model)
if result.Error != nil {
return nil, result.Error
}
return &model, nil
}
func (sr *SubscriptionModelsRepository) GetSubscriptionModelNames() ([]string, error) {
var names []string
if err := database.DB.Model(&models.SubscriptionModel{}).Pluck("name", &names).Error; err != nil {
return []string{}, err
}
return names, nil
}
func (sr *SubscriptionModelsRepository) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) {
var subscriptions []models.SubscriptionModel
result := database.DB.Where(where).Find(&subscriptions)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &subscriptions, nil
}
func GetUsersBySubscription(subscriptionID uint) (*[]models.User, error) {
var users []models.User
err := database.DB.Preload("Membership").
Preload("Membership.SubscriptionModel").
Preload("BankAccount").
Preload("Licence").
Preload("Licence.Categories").
Joins("JOIN memberships ON users.id = memberships.user_id").
Joins("JOIN subscription_models ON memberships.subscription_model_id = subscription_models.id").
Where("subscription_models.id = ?", subscriptionID).
Find(&users).Error
if err != nil {
return nil, err
}
return &users, nil
}

View File

@@ -0,0 +1,97 @@
package repositories
import (
"GoMembership/internal/database"
"gorm.io/gorm"
"GoMembership/internal/models"
)
type SubscriptionsRepositoryInterface interface {
CreateSubscription(subscription *models.Subscription) (uint, error)
UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error)
GetSubscriptionNames() ([]string, error)
GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error)
// GetUsersBySubscription(id uint) (*[]models.Subscription, error)
DeleteSubscription(id *uint) error
}
type SubscriptionsRepository struct{}
func (sr *SubscriptionsRepository) CreateSubscription(subscription *models.Subscription) (uint, error) {
result := database.DB.Create(subscription)
if result.Error != nil {
return 0, result.Error
}
return subscription.ID, nil
}
func (sr *SubscriptionsRepository) UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error) {
result := database.DB.Model(&models.Subscription{ID: subscription.ID}).Updates(subscription)
if result.Error != nil {
return nil, result.Error
}
return subscription, nil
}
func (sr *SubscriptionsRepository) DeleteSubscription(id *uint) error {
result := database.DB.Delete(&models.Subscription{}, id)
if result.Error != nil {
return result.Error
}
return nil
}
func GetSubscriptionByName(modelname *string) (*models.Subscription, error) {
var model models.Subscription
result := database.DB.Where("name = ?", modelname).First(&model)
if result.Error != nil {
return nil, result.Error
}
return &model, nil
}
func (sr *SubscriptionsRepository) GetSubscriptionNames() ([]string, error) {
var names []string
if err := database.DB.Model(&models.Subscription{}).Pluck("name", &names).Error; err != nil {
return []string{}, err
}
return names, nil
}
func (sr *SubscriptionsRepository) GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error) {
var subscriptions []models.Subscription
result := database.DB.Where(where).Find(&subscriptions)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &subscriptions, nil
}
func GetUsersBySubscription(subscriptionID uint) (*[]models.User, error) {
var users []models.User
err := database.DB.Preload("Membership").
Preload("Membership.Subscription").
Preload("BankAccount").
Preload("Licence").
Preload("Licence.Categories").
Joins("JOIN memberships ON users.id = memberships.user_id").
Joins("JOIN subscriptions ON memberships.subscription_id = subscriptions.id").
Where("subscriptions.id = ?", subscriptionID).
Find(&users).Error
if err != nil {
return nil, err
}
return &users, nil
}

View File

@@ -37,7 +37,7 @@ func Run(db *gorm.DB) {
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{}
var subscriptionRepo repositories.SubscriptionModelsRepositoryInterface = &repositories.SubscriptionModelsRepository{}
var subscriptionRepo repositories.SubscriptionsRepositoryInterface = &repositories.SubscriptionsRepository{}
membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo}
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}

View File

@@ -59,8 +59,8 @@ func (s *CarService) FromID(id uint) (*models.Car, error) {
// GetAll retrieves all cars
func (s *CarService) GetAll() (*[]models.Car, error) {
var cars []models.Car
if err := s.DB.Find(&cars).Error; err != nil {
cars, err := models.GetAllCars(s.DB)
if err != nil {
return nil, err
}
return &cars, nil

View File

@@ -46,7 +46,7 @@ func (s *EmailService) SendEmail(to string, subject string, body string, bodyTXT
func ParseTemplate(filename string, data interface{}) (string, error) {
// Read the email template file
logger.Error.Printf("Data: %#v", data)
templateDir := config.Templates.MailPath
tpl, err := template.ParseFiles(templateDir + "/" + filename)
if err != nil {
@@ -71,15 +71,18 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
LastName string
Token string
BASEURL string
ID uint
UserID uint
Logo string
}{
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
BASEURL: config.Site.BaseURL,
ID: user.ID,
UserID: user.ID,
Logo: config.Templates.LogoURI,
}
logger.Error.Printf("USERIID: %#v", user.ID)
subject := constants.MailVerificationSubject
body, err := ParseTemplate("mail_verification.tmpl", data)
if err != nil {
@@ -98,6 +101,7 @@ func (s *EmailService) SendGrantBackendAccessEmail(user *models.User, token *str
BASEURL string
FRONTEND_PATH string
UserID uint
Logo string
}{
FirstName: user.FirstName,
LastName: user.LastName,
@@ -105,6 +109,7 @@ func (s *EmailService) SendGrantBackendAccessEmail(user *models.User, token *str
FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
Logo: config.Templates.LogoURI,
}
subject := constants.MailGrantBackendAccessSubject
@@ -130,6 +135,7 @@ func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string)
BASEURL string
FRONTEND_PATH string
UserID uint
Logo string
}{
FirstName: user.FirstName,
LastName: user.LastName,
@@ -137,6 +143,7 @@ func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string)
FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
Logo: config.Templates.LogoURI,
}
subject := constants.MailChangePasswordSubject
@@ -169,10 +176,10 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
}{
Company: user.Company,
FirstName: user.FirstName,
MembershipModel: user.Membership.SubscriptionModel.Name,
MembershipModel: user.Membership.Subscription.Name,
MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
MembershipFee: float32(user.Membership.Subscription.MonthlyFee),
RentalFee: float32(user.Membership.Subscription.HourlyRate),
BASEURL: config.Site.BaseURL,
WebsiteTitle: config.Site.WebsiteTitle,
Logo: config.Templates.LogoURI,
@@ -216,10 +223,10 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
Company: user.Company,
FirstName: user.FirstName,
LastName: user.LastName,
MembershipModel: user.Membership.SubscriptionModel.Name,
MembershipModel: user.Membership.Subscription.Name,
MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
MembershipFee: float32(user.Membership.Subscription.MonthlyFee),
RentalFee: float32(user.Membership.Subscription.HourlyRate),
Address: user.Address,
ZipCode: user.ZipCode,
City: user.City,

View File

@@ -11,17 +11,17 @@ import (
type MembershipServiceInterface interface {
RegisterMembership(membership *models.Membership) (uint, error)
FindMembershipByUserID(userID uint) (*models.Membership, error)
RegisterSubscription(subscription *models.SubscriptionModel) (uint, error)
UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error)
RegisterSubscription(subscription *models.Subscription) (uint, error)
UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error)
DeleteSubscription(id *uint, name *string) error
GetSubscriptionModelNames() ([]string, error)
GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
GetSubscriptionNames() ([]string, error)
GetSubscriptionByName(modelname *string) (*models.Subscription, error)
GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error)
}
type MembershipService struct {
Repo repositories.MembershipRepositoryInterface
SubscriptionRepo repositories.SubscriptionModelsRepositoryInterface
SubscriptionRepo repositories.SubscriptionsRepositoryInterface
}
func (service *MembershipService) RegisterMembership(membership *models.Membership) (uint, error) {
@@ -29,7 +29,7 @@ func (service *MembershipService) RegisterMembership(membership *models.Membersh
return service.Repo.CreateMembership(membership)
}
func (service *MembershipService) UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error) {
func (service *MembershipService) UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error) {
existingSubscription, err := repositories.GetSubscriptionByName(&subscription.Name)
if err != nil {
@@ -82,19 +82,19 @@ func (service *MembershipService) FindMembershipByUserID(userID uint) (*models.M
}
// Membership_Subscriptions
func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) {
return service.SubscriptionRepo.CreateSubscriptionModel(subscription)
func (service *MembershipService) RegisterSubscription(subscription *models.Subscription) (uint, error) {
return service.SubscriptionRepo.CreateSubscription(subscription)
}
func (service *MembershipService) GetSubscriptionModelNames() ([]string, error) {
return service.SubscriptionRepo.GetSubscriptionModelNames()
func (service *MembershipService) GetSubscriptionNames() ([]string, error) {
return service.SubscriptionRepo.GetSubscriptionNames()
}
func (service *MembershipService) GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error) {
func (service *MembershipService) GetSubscriptionByName(modelname *string) (*models.Subscription, error) {
return repositories.GetSubscriptionByName(modelname)
}
func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) {
func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error) {
if where == nil {
where = map[string]interface{}{}
}

View File

@@ -74,15 +74,13 @@ func (s *UserService) Update(user *models.User) (*models.User, error) {
}
user.BankAccount.ID = existingUser.BankAccount.ID
user.SetPassword(user.Password)
// Validate subscription model
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil {
return nil, errors.ErrSubscriptionNotFound
}
user.Membership.SubscriptionModel = *selectedModel
user.Membership.SubscriptionModelID = selectedModel.ID
user.Membership.Subscription = *selectedModel
user.Membership.SubscriptionID = selectedModel.ID
if err := user.Update(s.DB); err != nil {
if err == gorm.ErrRecordNotFound {
@@ -97,14 +95,13 @@ func (s *UserService) Update(user *models.User) (*models.User, error) {
}
func (s *UserService) Register(user *models.User) (id uint, token string, err error) {
user.SetPassword(user.Password)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil {
return 0, "", errors.ErrSubscriptionNotFound
}
user.Membership.SubscriptionModel = *selectedModel
user.Membership.SubscriptionModelID = selectedModel.ID
user.Membership.Subscription = *selectedModel
user.Membership.SubscriptionID = selectedModel.ID
user.Status = constants.UnverifiedStatus
user.BankAccount.MandateDateSigned = time.Now()
v, err := user.SetVerification(constants.VerificationTypes.Email)

View File

@@ -30,10 +30,6 @@ func GenerateRandomString(length int) (string, error) {
return base64.URLEncoding.EncodeToString(bytes), nil
}
func GenerateVerificationToken() (string, error) {
return GenerateRandomString(32)
}
func DecodeMail(message string) (*Email, error) {
msg, err := mail.ReadMessage(strings.NewReader(message))
if err != nil {

View File

@@ -44,7 +44,7 @@ func HandleUserUpdateError(c *gin.Context, err error) {
case errors.ErrDuplicateEntry:
RespondWithError(c, err, "User Unique constraint failed", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.Duplicate)
case errors.ErrSubscriptionNotFound:
RespondWithError(c, err, "Couldn't find subscription", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.NotFound)
RespondWithError(c, err, "Couldn't find subscription", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.NotFound)
default:
RespondWithError(c, err, "Couldn't update user", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
}
@@ -53,30 +53,30 @@ func HandleUserUpdateError(c *gin.Context, err error) {
func HandleSubscriptionDeleteError(c *gin.Context, err error) {
switch err {
case errors.ErrNoData:
RespondWithError(c, err, "Missing subscription name during deletion", http.StatusExpectationFailed, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Invalid)
RespondWithError(c, err, "Missing subscription name during deletion", http.StatusExpectationFailed, errors.Responses.Fields.Subscription, errors.Responses.Keys.Invalid)
case errors.ErrSubscriptionNotFound:
RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.NotFound)
RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.NotFound)
case errors.ErrInvalidSubscriptionData:
RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Invalid)
RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.Subscription, errors.Responses.Keys.Invalid)
case errors.ErrSubscriptionInUse:
RespondWithError(c, err, "Subscription is in use by at least one user", http.StatusExpectationFailed, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InUse)
RespondWithError(c, err, "Subscription is in use by at least one user", http.StatusExpectationFailed, errors.Responses.Fields.Subscription, errors.Responses.Keys.InUse)
default:
RespondWithError(c, err, "Error during subscription Deletion", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError)
RespondWithError(c, err, "Error during subscription Deletion", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
}
}
func HandleSubscriptionUpdateError(c *gin.Context, err error) {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Duplicate)
RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.Subscription, errors.Responses.Keys.Duplicate)
} else {
switch err {
case errors.ErrSubscriptionNotFound:
RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.NotFound)
RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.NotFound)
case errors.ErrInvalidSubscriptionData:
RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Invalid)
RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.Subscription, errors.Responses.Keys.Invalid)
default:
RespondWithError(c, err, "Couldn't update subscription", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError)
RespondWithError(c, err, "Couldn't update subscription", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
}
}
}

View File

@@ -10,17 +10,17 @@ import (
)
func validateMembership(db *gorm.DB, user *models.User, sl validator.StructLevel) {
if user.Membership.SubscriptionModel.RequiredMembershipField != "" {
switch user.Membership.SubscriptionModel.RequiredMembershipField {
if user.Membership.Subscription.RequiredMembershipField != "" {
switch user.Membership.Subscription.RequiredMembershipField {
case "ParentMembershipID":
if err := CheckParentMembershipID(db, user); err != nil {
logger.Error.Printf("Error ParentMembershipValidation: %v", err.Error())
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.SubscriptionModel.RequiredMembershipField,
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.Subscription.RequiredMembershipField,
"RequiredMembershipField", "invalid", "")
}
default:
logger.Error.Printf("Error no matching RequiredMembershipField: %v", errors.ErrInvalidValue.Error())
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.SubscriptionModel.RequiredMembershipField,
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.Subscription.RequiredMembershipField,
"RequiredMembershipField", "not_implemented", "")
}
}

View File

@@ -1,8 +1,6 @@
package validation
import (
"GoMembership/internal/models"
"github.com/gin-gonic/gin/binding"
"gorm.io/gorm"
@@ -15,7 +13,7 @@ func SetupValidators(db *gorm.DB) {
v.RegisterValidation("safe_content", ValidateSafeContent)
// Register struct-level validations
v.RegisterStructValidation(ValidateUserFactory(db), models.User{})
v.RegisterStructValidation(ValidateSubscription, models.SubscriptionModel{})
// v.RegisterStructValidation(ValidateUserFactory(db), models.User{})
// v.RegisterStructValidation(ValidateSubscription, models.Subscription{})
}
}

View File

@@ -3,19 +3,17 @@ package validation
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/pkg/logger"
"github.com/go-playground/validator/v10"
)
// ValidateNewSubscription validates a new subscription model being created
func ValidateSubscription(sl validator.StructLevel) {
subscription := sl.Current().Interface().(models.SubscriptionModel)
subscription := sl.Current().Interface().(models.Subscription)
if subscription.Name == "" {
sl.ReportError(subscription.Name, "Name", "name", "required", "")
}
logger.Error.Printf("parent.type.name: %#v", sl.Parent().Type().Name())
if sl.Parent().Type().Name() == "" {
// This is modifying a subscription directly
if subscription.Details == "" {

View File

@@ -30,16 +30,16 @@ func ValidateUserFactory(db *gorm.DB) validator.StructLevelFunc {
func validateUser(db *gorm.DB, sl validator.StructLevel) {
user := sl.Current().Interface().(models.User)
// validate subscriptionModel
if user.Membership.SubscriptionModel.Name == "" {
sl.ReportError(user.Membership.SubscriptionModel.Name, "subscription.name", "name", "required", "")
// validate subscription
if user.Membership.Subscription.Name == "" {
sl.ReportError(user.Membership.Subscription.Name, "subscription.name", "name", "required", "")
} else {
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil {
logger.Error.Printf("Error finding subscription model for user %v: %v", user.Email, err)
sl.ReportError(user.Membership.SubscriptionModel.Name, "subscription.name", "name", "invalid", "")
sl.ReportError(user.Membership.Subscription.Name, "subscription.name", "name", "invalid", "")
} else {
user.Membership.SubscriptionModel = *selectedModel
user.Membership.Subscription = *selectedModel
}
}
if user.IsSupporter() {

View File

@@ -31,7 +31,7 @@ type ValidationKeys struct {
InternalServerError string
InvalidJSON string
InvalidUserID string
InvalidSubscriptionModel string
InvalidSubscription string
Unauthorized string
UserNotFoundWrongPassword string
JwtGenerationFailed string
@@ -48,7 +48,7 @@ type ValidationKeys struct {
type ValidationFields struct {
General string
ParentMembershipID string
SubscriptionModel string
Subscription string
Login string
Email string
User string
@@ -66,7 +66,7 @@ var Responses = struct {
InternalServerError: "server.error.internal_server_error",
InvalidJSON: "server.error.invalid_json",
InvalidUserID: "server.validation.invalid_user_id",
InvalidSubscriptionModel: "server.validation.invalid_subscription_model",
InvalidSubscription: "server.validation.invalid_subscription",
Unauthorized: "server.error.unauthorized",
UserNotFoundWrongPassword: "server.validation.user_not_found_or_wrong_password",
JwtGenerationFailed: "server.error.jwt_generation_failed",
@@ -82,7 +82,7 @@ var Responses = struct {
Fields: ValidationFields{
General: "server.general",
ParentMembershipID: "parent_membership_id",
SubscriptionModel: "subscription_model",
Subscription: "subscription",
Login: "user.login",
Email: "user.email",
User: "user.user",

View File

@@ -36,7 +36,7 @@
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg"
src="{{.Logo}}"
style="
outline: none;
border: none;

View File

@@ -40,7 +40,7 @@
target="_blank"
><img
alt="{{.WebsiteTitle}}"
src="{{.BASEURL}}{{.Logo}}"
src="{{.Logo}}"
style="
outline: none;
border: none;

View File

@@ -36,7 +36,7 @@
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg"
src="{{.Logo}}"
style="
outline: none;
border: none;

View File

@@ -36,7 +36,7 @@
target="_blank"
><img
alt="{{.WebsiteTitle}}"
src="{{.BASEURL}}{{.Logo}}"
src="{{.Logo}}"
style="
outline: none;
border: none;

View File

@@ -36,7 +36,7 @@
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg"
src="{{.Logo}}"
style="
outline: none;
border: none;
@@ -70,7 +70,7 @@
</div>
<div style="text-align: center; padding: 16px 24px 16px 24px">
<a
href="{{.BASEURL}}/api/users/verify/{{.ID}}?token={{.Token}}"
href="{{.BASEURL}}/api/users/verify/{{.UserID}}?token={{.Token}}"
style="
color: #ffffff;
font-size: 26px;

View File

@@ -8,7 +8,7 @@ noch Ihre Emailadresse indem Sie hier klicken:
E-Mail Adresse bestätigen
{{.BASEURL}}/api/users/verify/{{.ID}}?token={{.Token}}
{{.BASEURL}}/api/users/verify/{{.UserID}}?token={{.Token}}
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir
Ihnen alle weiteren Informationen zu. Wir freuen uns auf die

View File

@@ -36,7 +36,7 @@
target="_blank"
><img
alt="{{.WebsiteTitle}}"
src="{{.BASEURL}}{{.Logo}}"
src="{{.Logo}}"
style="
outline: none;
border: none;
@@ -80,22 +80,61 @@
</ul>
</li>
<li>
<strong>Mitgliedsbeitrag</strong>: Solange wir noch kein
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
<strong>Führerscheinverifikation</strong>:
Dein Führerschein wird bei der Anmeldung bei unserem Dienstleister Moqo verifiziert.
</li>
<li>
<strong>Führerscheinverifikation</strong>: Weitere
Informationen zur Verifikation deines Führerscheins folgen
in Kürze. Du musst nichts weiter tun, wir werden uns bei dir
melden, sobald es notwendig ist.
Bitte melde Dich nun noch auf der Webseite von Moqo für Deinen gewählten
Tarif an:
</li>
<li>
<strong>Moqo App</strong>: Wir werden die Moqo App nutzen,
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen
ersten Eindruck von dem Buchungsvorgang haben möchtest,
schaue Dir gerne dieses kurze Video an:
</li>
<strong>Moqo Anmeldung(Bitte UNBEDINGT AUSFÜLLEN, sonst ist kein Ausleihen möglich)</strong>:
<div style="text-align: center; padding: 0px 24px 16px 24px">
<a
href="https://portal.moqo.de/js_sign_up/18013114#/subscription-selection"
style="
color: #ffffff;
font-size: 16px;
font-weight: bold;
background-color: #f45050;
border-radius: 4px;
display: inline-block;
padding: 12px 20px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 20px;
mso-font-width: -100%;
mso-text-raise: 30;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>MOQO Anmeldung</span
><span
><!--[if mso
]><i
style="letter-spacing: 20px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<li>
<strong>Moqo App</strong>:
Wir nutzen die Moqo App, damit Du das Fahrzeug mit Deinem Handy
entsperren und Deine Buchungen verwalten kannst.
Wenn Du schon mal einen ersten Eindruck von dem Buchungsvorgang
haben möchtest, schaue Dir gerne dieses kurze Video an:
</li>
</ul>
</div>
@@ -272,8 +311,8 @@
</ul>
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen.
Wir danken Dir herzlich für Dein Vertrauen und freuen uns drauf, Dich bald
Dörpsmobil fahrend in Hasloh zu sehen!
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
<p>Mit freundlichen Grüßen,</p>

View File

@@ -12,23 +12,24 @@ Hier einige wichtige Informationen für dich:
Preis/Monat: {{.MembershipFee}}
Preis/h: {{.RentalFee}}
Mitgliedsbeitrag: Solange wir noch kein
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
Führerscheinverifikation: Dein Führerschein wird bei der Anmeldung
bei unserem Dienstleister Moqo verifiziert.
Führerscheinverifikation: Weitere Informationen zur Verifikation
deines Führerscheins folgen in Kürze. Du musst nichts weiter tun,
wir werden uns bei dir melden, sobald es notwendig ist.
Bitte melde Dich nun noch auf der Webseite von Moqo für Deinen gewählten
Tarif an:
Moqo App:
Wir werden die Moqo App nutzen,
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen
ersten Eindruck von dem Buchungsvorgang haben möchtest,
schaue Dir gerne dieses kurze Video an:
Moqo Anmeldung(Bitte UNBEDINGT AUSFÜLLEN, sonst ist kein Ausleihen möglich):
https://portal.moqo.de/js_sign_up/18013114#/subscription-selection
Das Ausleihen:
Wir nutzen die Moqo App, damit Du das Fahrzeug mit Deinem Handy
entsperren und Deine Buchungen verwalten kannst.
Wenn Du schon mal einen ersten Eindruck von dem Buchungsvorgang
haben möchtest, schaue Dir gerne dieses kurze Video an:
Moqo App Nutzung
https://www.youtube.com/shorts/ZMKUX0uyOps
Dörpsmobil:
Wir sind nicht alleine sondern Mitglied in einem Schleswig-Holstein
weiten Netz an gemeinnützigen Carsharing Anbietern. Für mehr
@@ -49,8 +50,8 @@ du dich jederzeit an unsere Vorsitzende wenden:
E-Mail: vorstand@carsharing-hasloh.de
Telefon: +49 176 5013 4256
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen.
Wir danken Dir herzlich für Dein Vertrauen und freuen uns drauf, Dich bald
Dörpsmobil fahrend in Hasloh zu sehen!
Mit freundlichen Grüßen,
Dein Carsharing Hasloh Team