Compare commits

...

40 Commits

Author SHA1 Message Date
philipp lang 37c9021e76 Fix seeder in docker container 2025-04-04 00:18:26 +02:00
philipp lang 2e0b42bfbd Compile adrema-form scripts in docker build 2025-04-04 00:18:02 +02:00
philipp lang 3c7cd1173a Fix casing of FROM and AS in Dockerfile 2025-04-04 00:17:28 +02:00
philipp lang 0c14ebb229 Update README 2025-04-04 00:13:22 +02:00
philipp lang 0679a09d26 Lint 2025-04-04 00:12:29 +02:00
philipp lang 2dd0c3d2a0 Add update for bank account 2025-04-04 00:12:29 +02:00
philipp lang 21964e9665 Set permanent bank account of member 2025-04-04 00:12:29 +02:00
philipp lang 5afe8613e1 Lint 2025-04-04 00:12:29 +02:00
philipp lang c047f8da63 Update README 2025-04-03 23:07:30 +02:00
philipp lang 9097f0bbd9 Remove restart always 2025-04-03 23:07:16 +02:00
philipp lang 3f44fbe5b0 Add seeding for database 2025-04-03 23:07:03 +02:00
philipp lang 8e1f7869e2 Fix UserSeeder 2025-04-03 23:04:26 +02:00
philipp lang abf4158256 Fix phpstan 2025-04-03 00:11:32 +02:00
philipp lang f2d59e4535 Update phpstan baseline 2025-04-03 00:11:32 +02:00
philipp lang a75b4a7808 Lint 2025-04-03 00:11:32 +02:00
philipp lang ecf3cc998c Update tex package 2025-04-03 00:11:32 +02:00
philipp lang 7202cd60f5 Add doctypes 2025-04-03 00:11:32 +02:00
philipp lang 9d5c71adcd Add Data Eloquent casting 2025-04-03 00:11:32 +02:00
philipp lang eb00d928fc Remove deprecated willReturnVar 2025-04-03 00:11:32 +02:00
philipp lang 6683990755 Lint 2025-04-03 00:11:32 +02:00
philipp lang 2a5f4832e1 Add configs for User Seeder 2025-04-03 00:11:32 +02:00
philipp lang 719b1920c9 Lint 2025-04-03 00:11:32 +02:00
philipp lang 3260202465 Update larastan 2025-04-03 00:11:32 +02:00
philipp lang 9b995641b6 Update laravel-nami 2025-04-03 00:11:32 +02:00
philipp lang 2d5921fe1d Mod copydb 2025-04-03 00:11:32 +02:00
philipp lang efa9eeccbe Update laravel-nami 2025-04-03 00:11:32 +02:00
philipp lang b33072ad20 Lint 2025-04-03 00:11:32 +02:00
philipp lang 2e77823c3a --wip-- [skip ci] 2025-04-03 00:11:32 +02:00
philipp lang 9a5d9ae30e Add Bank account to NAmi put action 2025-04-03 00:11:32 +02:00
philipp lang d607c8beac Add scout driver config to tests 2025-04-03 00:11:32 +02:00
philipp lang 1a86ea7ea5 Update laravel-nami 2025-04-03 00:11:32 +02:00
philipp lang a157f2b656 Update composer packages 2025-04-03 00:11:32 +02:00
philipp lang e3c7dad51e Update npm packages 2025-04-03 00:11:32 +02:00
philipp lang 749e3dcd7c Mod stammsilva server name 2025-04-01 21:14:00 +02:00
philipp lang 4417008d22 Update CHANGELOG 2025-04-01 20:49:50 +02:00
philipp lang b38591b87c Remove page limit when synching maildispatchers 2025-04-01 20:26:41 +02:00
philipp lang 17e4fe5f82 Update CHANGELOG 2025-01-18 21:19:49 +01:00
philipp lang b2f3e4f1fd Fix: Split subscriptions between members 2025-01-18 21:17:04 +01:00
philipp lang 376c1ceb9b Update CHANGELOG 2025-01-18 13:05:43 +01:00
philipp lang 056b8f9ed6 Fixed: Dont update subscription from nami if fee id already matches 2025-01-18 12:03:36 +01:00
55 changed files with 3052 additions and 3090 deletions

View File

@ -1,4 +1,4 @@
FROM php:8.3.11-fpm as php FROM php:8.3.11-fpm AS php
WORKDIR /app WORKDIR /app
RUN ls /app RUN ls /app
RUN apt-get update RUN apt-get update

View File

@ -1,16 +1,17 @@
FROM composer:2.7.9 as composer FROM composer:2.7.9 AS composer
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN composer install --ignore-platform-reqs --no-dev RUN composer install --ignore-platform-reqs --no-dev
RUN php artisan telescope:publish RUN php artisan telescope:publish
RUN php artisan horizon:publish RUN php artisan horizon:publish
FROM node:20.15.0-slim as node FROM node:20.15.0-slim AS node
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules RUN cd packages/adrema-form && npm ci && npm run build && rm -R node_modules && cd ../../
RUN npm ci && npm run prod && npm run img && rm -R node_modules
FROM nginx:1.21.6-alpine as nginx FROM nginx:1.21.6-alpine AS nginx
WORKDIR /app WORKDIR /app
COPY --from=node /app /app COPY --from=node /app /app
COPY --from=composer /app/public/vendor /app/public/vendor COPY --from=composer /app/public/vendor /app/public/vendor

View File

@ -1,14 +1,15 @@
FROM composer:2.7.9 as composer FROM composer:2.7.9 AS composer
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN composer install --ignore-platform-reqs --no-dev RUN composer install --ignore-platform-reqs --no-dev
FROM node:20.15.0-slim as node FROM node:20.15.0-slim AS node
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules RUN cd packages/adrema-form && npm ci && npm run build && rm -R node_modules && cd ../../
RUN npm ci && npm run prod && npm run img && rm -R node_modules
FROM zoomyboy/adrema-base:latest as php FROM zoomyboy/adrema-base:latest AS php
COPY --chown=www-data:www-data . /app COPY --chown=www-data:www-data . /app
COPY --chown=www-data:www-data --from=node /app/public /app/public COPY --chown=www-data:www-data --from=node /app/public /app/public
COPY --chown=www-data:www-data --from=composer /app/vendor /app/vendor COPY --chown=www-data:www-data --from=composer /app/vendor /app/vendor

View File

@ -1,5 +1,17 @@
# Letzte Änderungen # Letzte Änderungen
### 1.12.7
- Fix: Synchronisation von allen Mitgliedern bei Mail-Verteilern - nicht nur den ersten 20
### 1.12.6
- Fix: Beiträge von Familienmitgliedern splitten
### 1.12.5
- Fix: Synchronisieren von bestehenden Beiträgen aus NaMi
### 1.12.4 ### 1.12.4
- Filter Mitglieder nach Verhaltenskodex - Filter Mitglieder nach Verhaltenskodex

View File

@ -1,4 +1,4 @@
@servers(['docker' => ['stammsilva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg-koeln@dpsg-koeln.de']]) @servers(['docker' => ['stamm-silva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg-koeln@dpsg-koeln.de']])
@task('deploy', ['on' => 'docker']) @task('deploy', ['on' => 'docker'])
cd $ADREMA_PATH cd $ADREMA_PATH

View File

@ -32,16 +32,16 @@ Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles
## Installation des Produktivsystems ## Installation des Produktivsystems
1. Herunterladen der Beispiel Docker-Compose 1. Verschieben der Docker-Compose
```cmd ```cmd
curl https://git.zoomyboy.de/silva/adrema/raw/branch/master/docker-compose.prod.yml -o docker-compose.yml mv docker-compose.prod.yml docker-compose.yml
``` ```
2. Herunterladen der Beispiel Environmentvariablen-Datei 2. Anwenden der Beispiel Environmentvariablen-Datei
```cmd ```cmd
curl https://git.zoomyboy.de/silva/adrema/raw/branch/master/.app.env.example -o .app.env mv .app.env.example .app.env
``` ```
3. In der `.app.env` notwendige Einstellungen vornehmen: 3. In der `.app.env` notwendige Einstellungen vornehmen:
@ -50,7 +50,8 @@ Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles
- Mail-Server Einstellungen `MAIL_PORT`, `MAIL_HOST`, `MAIL_USERNAME`, `MAIL_PASSWORD` und `MAIL_ENCRYPTION` anpassen - Mail-Server Einstellungen `MAIL_PORT`, `MAIL_HOST`, `MAIL_USERNAME`, `MAIL_PASSWORD` und `MAIL_ENCRYPTION` anpassen
- `MAIL_FROM_NAME`: Der Name, der als Absender von E-Mails gesetzt wird (z.B. `Stamm Bipi Service`) - `MAIL_FROM_NAME`: Der Name, der als Absender von E-Mails gesetzt wird (z.B. `Stamm Bipi Service`)
- `MAIL_FROM_ADDRESS`: Die dazu gehörige E-Mail-Adresse, die natürlich für antworten erreichbar sein sollte (z.B. `vorstand@stamm-bipi.de`) - `MAIL_FROM_ADDRESS`: Die dazu gehörige E-Mail-Adresse, die natürlich für antworten erreichbar sein sollte (z.B. `vorstand@stamm-bipi.de`)
- `DB_PASSWORD` und `MYSQL_PASSWORD`: Mit dem selben sicheren Passwort für die Datenbank versehen - `DB_PASSWORD` und `MYSQL_PASSWORD`: Mit dem selben neu erstellten, sicheren Passwort für die Datenbank versehen
- `MEILI_MASTER_KEY` Mit einem neu erstellten, sicheren Passwort versehen
- `USER_EMAIL` und `USER_PASSWORD`: Einstellen des standard Adrema Logins - `USER_EMAIL` und `USER_PASSWORD`: Einstellen des standard Adrema Logins
4. Container zur Gennerierung des App-Key starten 4. Container zur Gennerierung des App-Key starten
@ -99,28 +100,22 @@ Bei dem Setup wird im Daten-Verzeichniss ein Ordner `./data/setup` angelegt. Hie
git clone https://git.zoomyboy.de/silva/adrema.git git clone https://git.zoomyboy.de/silva/adrema.git
``` ```
2. Kopieren der Beispiel Docker-Compose für das Entwickeln und nach Wünschen anpassen 2. Kopieren der Beispiel Environmentvariablen-Datei
```cmd
cp docker-compose.dev.yml docker-compose.yml
```
3. Kopieren der Beispiel Environmentvariablen-Datei
```cmd ```cmd
cp .app.env.example .app.env cp .app.env.example .app.env
``` ```
4. Submodule aktuallisieren 3. Submodule aktuallisieren
```cmd ```cmd
git submodule update --init git submodule update --init
``` ```
5. Container erstellen 4. Container erstellen
```cmd ```cmd
docker compose build docker compose build
``` ```
6. Mit Schritt 3 und den folgenden der [Installation des Produktivsystems](#installation-des-produktivsystems) fortfahren 5. Mit Schritt 3 und den folgenden der [Installation des Produktivsystems](#installation-des-produktivsystems) fortfahren

View File

@ -22,7 +22,7 @@ class InsertMemberAction
{ {
$region = Region::firstWhere('nami_id', $member->regionId ?: -1); $region = Region::firstWhere('nami_id', $member->regionId ?: -1);
return Member::updateOrCreate(['nami_id' => $member->id], [ $payload = [
'firstname' => $member->firstname, 'firstname' => $member->firstname,
'lastname' => $member->lastname, 'lastname' => $member->lastname,
'joined_at' => $member->joinedAt, 'joined_at' => $member->joinedAt,
@ -51,7 +51,26 @@ class InsertMemberAction
'mitgliedsnr' => $member->memberId, 'mitgliedsnr' => $member->memberId,
'version' => $member->version, 'version' => $member->version,
'keepdata' => $member->keepdata, 'keepdata' => $member->keepdata,
]); ];
// Dont update subscription if fee id of existing member's subscription is already the same
if ($existing = Member::nami($member->id)) {
if ($existing->subscription && $existing->subscription->fee->nami_id === $member->feeId) {
$payload['subscription_id'] = $existing->subscription->id;
}
}
return tap(Member::updateOrCreate(['nami_id' => $member->id], $payload), function ($insertedMember) use ($member) {
$insertedMember->bankAccount->update([
'iban' => $member->bankAccount->iban,
'bic' => $member->bankAccount->bic,
'blz' => $member->bankAccount->blz,
'account_number' => $member->bankAccount->accountNumber,
'person' => $member->bankAccount->person,
'bank_name' => $member->bankAccount->bankName,
'nami_id' => $member->bankAccount->id,
]);
});
} }
public function getSubscription(NamiMember $member): ?Subscription public function getSubscription(NamiMember $member): ?Subscription

View File

@ -34,7 +34,7 @@ class Activity extends Model
} }
/** /**
* @return BelongsToMany<Subactivity> * @return BelongsToMany<Subactivity, $this>
*/ */
public function subactivities(): BelongsToMany public function subactivities(): BelongsToMany
{ {

View File

@ -17,7 +17,7 @@ class CourseMember extends Model
public $guarded = []; public $guarded = [];
/** /**
* @return BelongsTo<Course, self> * @return BelongsTo<Course, $this>
*/ */
public function course(): BelongsTo public function course(): BelongsTo
{ {
@ -25,7 +25,7 @@ class CourseMember extends Model
} }
/** /**
* @return BelongsTo<Member, self> * @return BelongsTo<Member, $this>
*/ */
public function member(): BelongsTo public function member(): BelongsTo
{ {

View File

@ -129,6 +129,7 @@ class Principal implements PrincipalBackendInterface
*/ */
public function findByUri($uri, $principalPrefix) public function findByUri($uri, $principalPrefix)
{ {
return null;
} }
/** /**

View File

@ -15,14 +15,13 @@ class Fee extends Model
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;
/** @var array<int, string> */
public $fillable = ['name', 'nami_id']; public $fillable = ['name', 'nami_id'];
/** @var bool */ /** @var bool */
public $timestamps = false; public $timestamps = false;
/** /**
* @return HasMany<Subscription> * @return HasMany<Subscription, $this>
*/ */
public function subscriptions(): HasMany public function subscriptions(): HasMany
{ {

View File

@ -3,11 +3,11 @@
namespace App\Form\Data; namespace App\Form\Data;
use App\Fileshare\Data\FileshareResourceData; use App\Fileshare\Data\FileshareResourceData;
use App\Form\Fields\Field;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
#[MapInputName(SnakeCaseMapper::class)] #[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)] #[MapOutputName(SnakeCaseMapper::class)]
@ -16,4 +16,13 @@ class ExportData extends Data
public function __construct(public ?FileshareResourceData $root = null, public ?string $groupBy = null, public ?string $toGroupField = null) public function __construct(public ?FileshareResourceData $root = null, public ?string $groupBy = null, public ?string $toGroupField = null)
{ {
} }
/**
* @param array<int, mixed> $arguments
* @return DataEloquentCast<self>
*/
public static function castUsing(array $arguments): DataEloquentCast
{
return new DataEloquentCast(static::class, $arguments);
}
} }

View File

@ -8,6 +8,7 @@ use Illuminate\Support\Collection;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer; use Spatie\LaravelData\Attributes\WithTransformer;
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
class FormConfigData extends Data class FormConfigData extends Data
{ {
@ -29,4 +30,13 @@ class FormConfigData extends Data
new FieldCollection([]) new FieldCollection([])
); );
} }
/**
* @param array<int, mixed> $arguments
* @return DataEloquentCast<self>
*/
public static function castUsing(array $arguments): DataEloquentCast
{
return new DataEloquentCast(static::class, $arguments);
}
} }

View File

@ -62,7 +62,7 @@ class Form extends Model implements HasMedia
} }
/** /**
* @return HasMany<Participant> * @return HasMany<Participant, $this>
*/ */
public function participants(): HasMany public function participants(): HasMany
{ {

View File

@ -33,7 +33,7 @@ class Participant extends Model implements Preventable
]; ];
/** /**
* @return BelongsTo<Form, self> * @return BelongsTo<Form, $this>
*/ */
public function form(): BelongsTo public function form(): BelongsTo
{ {
@ -41,7 +41,7 @@ class Participant extends Model implements Preventable
} }
/** /**
* @return HasMany<self> * @return HasMany<Participant, $this>
*/ */
public function children(): HasMany public function children(): HasMany
{ {
@ -49,7 +49,7 @@ class Participant extends Model implements Preventable
} }
/** /**
* @return BelongsTo<Member, self> * @return BelongsTo<Member, $this>
*/ */
public function member(): BelongsTo public function member(): BelongsTo
{ {

View File

@ -26,7 +26,7 @@ class Group extends Model
]; ];
/** /**
* @return BelongsTo<self, self> * @return BelongsTo<Group, $this>
*/ */
public function parent(): BelongsTo public function parent(): BelongsTo
{ {
@ -34,7 +34,7 @@ class Group extends Model
} }
/** /**
* @return HasMany<self> * @return HasMany<Group, $this>
*/ */
public function children(): HasMany public function children(): HasMany
{ {

View File

@ -18,5 +18,7 @@ class Authenticate extends Middleware
if (!$request->expectsJson()) { if (!$request->expectsJson()) {
return route('login'); return route('login');
} }
return null;
} }
} }

View File

@ -39,7 +39,7 @@ class MassStoreAction
foreach ($memberGroup as $members) { foreach ($memberGroup as $members) {
$invoice = Invoice::createForMember($members->first(), $members, $year); $invoice = Invoice::createForMember($members->first(), $members, $year);
$invoice->save(); $invoice->save();
$invoice->positions()->createMany($invoice->positions); $invoice->positions()->createMany($invoice->positions->toArray());
$invoices->push($invoice->fresh('positions')); $invoices->push($invoice->fresh('positions'));
} }

View File

@ -37,7 +37,7 @@ class Invoice extends Model
]; ];
/** /**
* @return HasMany<InvoicePosition> * @return HasMany<InvoicePosition, $this>
*/ */
public function positions(): HasMany public function positions(): HasMany
{ {
@ -49,7 +49,6 @@ class Invoice extends Model
*/ */
public static function createForMember(Member $member, Collection $members, int $year, Subscription $subscription = null): self public static function createForMember(Member $member, Collection $members, int $year, Subscription $subscription = null): self
{ {
$subscription = $subscription ?: $member->subscription;
$invoice = new self([ $invoice = new self([
'to' => [ 'to' => [
'name' => 'Familie ' . $member->lastname, 'name' => 'Familie ' . $member->lastname,
@ -66,7 +65,8 @@ class Invoice extends Model
$positions = collect([]); $positions = collect([]);
foreach ($members as $member) { foreach ($members as $member) {
foreach ($subscription->children as $child) { $memberSubscription = $subscription ?: $member->subscription;
foreach ($memberSubscription->children as $child) {
$positions->push([ $positions->push([
'description' => str($child->name)->replace('{name}', $member->firstname . ' ' . $member->lastname)->replace('{year}', (string) $year), 'description' => str($child->name)->replace('{name}', $member->firstname . ' ' . $member->lastname)->replace('{year}', (string) $year),
'price' => $child->amount, 'price' => $child->amount,

View File

@ -16,7 +16,7 @@ class InvoicePosition extends Model
public $guarded = []; public $guarded = [];
/** /**
* @return BelongsTo<Member, self> * @return BelongsTo<Member, $this>
*/ */
public function member(): BelongsTo public function member(): BelongsTo
{ {
@ -24,7 +24,7 @@ class InvoicePosition extends Model
} }
/** /**
* @return BelongsTo<Invoice, self> * @return BelongsTo<Invoice, $this>
*/ */
public function invoice(): BelongsTo public function invoice(): BelongsTo
{ {

View File

@ -6,6 +6,7 @@ use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataCollection;
use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
class Condition extends Data class Condition extends Data
{ {
@ -42,4 +43,13 @@ class Condition extends Data
{ {
return $this->mode === ConditionMode::ALL; return $this->mode === ConditionMode::ALL;
} }
/**
* @param array<int, mixed> $arguments
* @return DataEloquentCast<self>
*/
public static function castUsing(array $arguments): DataEloquentCast
{
return new DataEloquentCast(static::class, $arguments);
}
} }

View File

@ -3,6 +3,7 @@
namespace App\Lib\Editor; namespace App\Lib\Editor;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
/** @todo replace blocks with actual block data classes */ /** @todo replace blocks with actual block data classes */
class EditorData extends Data implements Editorable class EditorData extends Data implements Editorable
@ -83,4 +84,13 @@ class EditorData extends Data implements Editorable
{ {
return $this; return $this;
} }
/**
* @param array<int, mixed> $arguments
* @return DataEloquentCast<self>
*/
public static function castUsing(array $arguments): DataEloquentCast
{
return new DataEloquentCast(static::class, $arguments);
}
} }

View File

@ -8,7 +8,7 @@ use Spatie\LaravelData\Data;
/** /**
* @template T of Model * @template T of Model
* @property Builder $query * @property Builder<T> $query
*/ */
abstract class Filter extends Data abstract class Filter extends Data
{ {
@ -40,7 +40,7 @@ abstract class Filter extends Data
} }
/** /**
* @return static(self<T>) * @return static
*/ */
public function toDefault(): self public function toDefault(): self
{ {

View File

@ -8,7 +8,7 @@ use Spatie\LaravelData\Data;
/** /**
* @template T of Model * @template T of Model
* @property Builder $query * @property Builder<T> $query
*/ */
abstract class ScoutFilter extends Data abstract class ScoutFilter extends Data
{ {

View File

@ -25,7 +25,7 @@ class ResyncAction
*/ */
public function getResults(Maildispatcher $dispatcher): Collection public function getResults(Maildispatcher $dispatcher): Collection
{ {
return FilterScope::fromPost($dispatcher->filter)->getQuery()->get() return FilterScope::fromPost($dispatcher->filter)->noPageLimit()->getQuery()->get()
->filter(fn ($member) => $member->email || $member->email_parents) ->filter(fn ($member) => $member->email || $member->email_parents)
->map(fn ($member) => MailEntry::from(['email' => $member->email ?: $member->email_parents])) ->map(fn ($member) => MailEntry::from(['email' => $member->email ?: $member->email_parents]))
->unique(fn ($member) => $member->email); ->unique(fn ($member) => $member->email);

View File

@ -23,7 +23,7 @@ class Maildispatcher extends Model
]; ];
/** /**
* @return BelongsTo<Mailgateway, self> * @return BelongsTo<Mailgateway, $this>
*/ */
public function gateway(): BelongsTo public function gateway(): BelongsTo
{ {

View File

@ -68,9 +68,9 @@ abstract class Type
} }
/** /**
* @return array<string, array<string, mixed>> * @return mixed
*/ */
public function toResource(): array public function toResource()
{ {
return [ return [
'cls' => get_class($this), 'cls' => get_class($this),
@ -93,8 +93,8 @@ abstract class Type
} }
$this->list($name, $domain) $this->list($name, $domain)
->filter(fn ($listEntry) => $results->doesntContain(fn ($r) => $r->is($listEntry))) ->filter(fn ($listEntry) => $results->doesntContain(fn ($r) => $r->is($listEntry)))
->each(fn ($listEntry) => $this->remove($name, $domain, $listEntry->email)); ->each(fn ($listEntry) => $this->remove($name, $domain, $listEntry->email));
} }
/** /**

View File

@ -10,6 +10,7 @@ use App\Member\Member;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
use App\Subactivity; use App\Subactivity;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Data\BankAccount;
use Zoomyboy\LaravelNami\Data\Member as NamiMember; use Zoomyboy\LaravelNami\Data\Member as NamiMember;
class NamiPutMemberAction class NamiPutMemberAction
@ -47,6 +48,16 @@ class NamiPutMemberAction
'id' => $member->nami_id, 'id' => $member->nami_id,
'version' => $member->version, 'version' => $member->version,
'keepdata' => $member->keepdata, 'keepdata' => $member->keepdata,
'bankAccount' => BankAccount::from([
'bankName' => $member->bankAccount->bank_name,
'id' => $member->bankAccount->nami_id,
'memberId' => $member->mitgliedsnr,
'iban' => $member->bankAccount->iban,
'bic' => $member->bankAccount->bic,
'blz' => $member->bankAccount->blz,
'person' => $member->bankAccount->person,
'accountNumber' => $member->bankAccount->account_number,
]),
]); ]);
$response = $api->putMember($namiMember, $activity ? $activity->nami_id : null, $subactivity ? $subactivity->nami_id : null); $response = $api->putMember($namiMember, $activity ? $activity->nami_id : null, $subactivity ? $subactivity->nami_id : null);
Member::withoutEvents(function () use ($response, $member) { Member::withoutEvents(function () use ($response, $member) {

View File

@ -0,0 +1,17 @@
<?php
namespace App\Member;
use Database\Factories\Member\BankAccountFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BankAccount extends Model
{
/** @use HasFactory<BankAccountFactory> */
use HasFactory;
public $guarded = [];
public $primaryKey = 'member_id';
}

View File

@ -9,7 +9,8 @@ use Zoomyboy\LaravelNami\Data\Member as NamiMember;
use Zoomyboy\LaravelNami\Data\MembershipEntry as NamiMembershipEntry; use Zoomyboy\LaravelNami\Data\MembershipEntry as NamiMembershipEntry;
use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\DataCollectionOf;
class FullMember extends Data { class FullMember extends Data
{
/** /**
* @param DataCollection<int, NamiCourse> $courses * @param DataCollection<int, NamiCourse> $courses
* @param DataCollection<int, NamiMembershipEntry> $memberships * @param DataCollection<int, NamiMembershipEntry> $memberships
@ -20,6 +21,6 @@ class FullMember extends Data {
public DataCollection $courses, public DataCollection $courses,
#[DataCollectionOf(NamiMembershipEntry::class)] #[DataCollectionOf(NamiMembershipEntry::class)]
public DataCollection $memberships, public DataCollection $memberships,
) {} ) {
}
} }

View File

@ -34,6 +34,7 @@ use Zoomyboy\Osm\HasGeolocation;
use Zoomyboy\Phone\HasPhoneNumbers; use Zoomyboy\Phone\HasPhoneNumbers;
use App\Prevention\Enums\Prevention; use App\Prevention\Enums\Prevention;
use Database\Factories\Member\MemberFactory; use Database\Factories\Member\MemberFactory;
use Illuminate\Database\Eloquent\Relations\HasOne;
/** /**
* @property string $subscription_name * @property string $subscription_name
@ -195,7 +196,7 @@ class Member extends Model implements Geolocatable
// ---------------------------------- Relations ---------------------------------- // ---------------------------------- Relations ----------------------------------
/** /**
* @return BelongsTo<Country, self> * @return BelongsTo<Country, $this>
*/ */
public function country(): BelongsTo public function country(): BelongsTo
{ {
@ -203,7 +204,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return BelongsTo<Gender, self> * @return BelongsTo<Gender, $this>
*/ */
public function gender(): BelongsTo public function gender(): BelongsTo
{ {
@ -211,7 +212,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return BelongsTo<Region, self> * @return BelongsTo<Region, $this>
*/ */
public function region(): BelongsTo public function region(): BelongsTo
{ {
@ -222,7 +223,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return HasMany<InvoicePosition> * @return HasMany<InvoicePosition, $this>
*/ */
public function invoicePositions(): HasMany public function invoicePositions(): HasMany
{ {
@ -230,7 +231,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return BelongsTo<Confession, self> * @return BelongsTo<Confession, $this>
*/ */
public function confession(): BelongsTo public function confession(): BelongsTo
{ {
@ -238,7 +239,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return BelongsTo<Nationality, self> * @return BelongsTo<Nationality, $this>
*/ */
public function nationality(): BelongsTo public function nationality(): BelongsTo
{ {
@ -246,7 +247,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return BelongsTo<Subscription, self> * @return BelongsTo<Subscription, $this>
*/ */
public function subscription(): BelongsTo public function subscription(): BelongsTo
{ {
@ -254,7 +255,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return BelongsTo<Group, self> * @return BelongsTo<Group, $this>
*/ */
public function group(): BelongsTo public function group(): BelongsTo
{ {
@ -262,7 +263,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return HasMany<CourseMember> * @return HasMany<CourseMember, $this>
*/ */
public function courses(): HasMany public function courses(): HasMany
{ {
@ -270,7 +271,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return HasMany<Membership> * @return HasMany<Membership, $this>
*/ */
public function memberships(): HasMany public function memberships(): HasMany
{ {
@ -278,7 +279,7 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return HasMany<Membership> * @return HasMany<Membership, $this>
*/ */
public function leaderMemberships(): HasMany public function leaderMemberships(): HasMany
{ {
@ -286,21 +287,34 @@ class Member extends Model implements Geolocatable
} }
/** /**
* @return HasMany<Membership> * @return HasMany<Membership, $this>
*/ */
public function ageGroupMemberships(): HasMany public function ageGroupMemberships(): HasMany
{ {
return $this->memberships()->isAgeGroup()->active(); return $this->memberships()->isAgeGroup()->active();
} }
/**
* @return HasOne<BankAccount, $this>
*/
public function bankAccount(): HasOne
{
return $this->hasOne(BankAccount::class);
}
public static function booted() public static function booted()
{ {
static::created(function (self $model): void {
$model->bankAccount()->create([]);
});
static::deleting(function (self $model): void { static::deleting(function (self $model): void {
$model->memberships->each->delete(); $model->memberships->each->delete();
$model->courses->each->delete(); $model->courses->each->delete();
$model->invoicePositions->each(function ($position) { $model->invoicePositions->each(function ($position) {
$position->delete(); $position->delete();
}); });
$model->bankAccount()->delete();
}); });
} }

View File

@ -32,7 +32,7 @@ class Membership extends Model
]; ];
/** /**
* @return BelongsTo<Activity, self> * @return BelongsTo<Activity, $this>
*/ */
public function activity(): BelongsTo public function activity(): BelongsTo
{ {
@ -40,7 +40,7 @@ class Membership extends Model
} }
/** /**
* @return BelongsTo<Group, self> * @return BelongsTo<Group, $this>
*/ */
public function group(): BelongsTo public function group(): BelongsTo
{ {
@ -48,7 +48,7 @@ class Membership extends Model
} }
/** /**
* @return BelongsTo<Subactivity, self> * @return BelongsTo<Subactivity, $this>
*/ */
public function subactivity(): BelongsTo public function subactivity(): BelongsTo
{ {
@ -56,7 +56,7 @@ class Membership extends Model
} }
/** /**
* @return BelongsTo<Member, self> * @return BelongsTo<Member, $this>
*/ */
public function member(): BelongsTo public function member(): BelongsTo
{ {

View File

@ -14,9 +14,6 @@ class Subscription extends Model
/** @use HasFactory<SubscriptionFactory> */ /** @use HasFactory<SubscriptionFactory> */
use HasFactory; use HasFactory;
/**
* @var array<int, string>
*/
public $fillable = ['name', 'fee_id']; public $fillable = ['name', 'fee_id'];
public function getAmount(): int public function getAmount(): int
@ -25,7 +22,7 @@ class Subscription extends Model
} }
/** /**
* @return BelongsTo<Fee, self> * @return BelongsTo<Fee, $this>
*/ */
public function fee(): BelongsTo public function fee(): BelongsTo
{ {
@ -33,7 +30,7 @@ class Subscription extends Model
} }
/** /**
* @return HasMany<SubscriptionChild> * @return HasMany<SubscriptionChild, $this>
*/ */
public function children(): HasMany public function children(): HasMany
{ {

View File

@ -16,9 +16,6 @@ class Subactivity extends Model
use HasNamiField; use HasNamiField;
use Sluggable; use Sluggable;
/**
* @var array<int, string>
*/
public $fillable = ['is_age_group', 'is_filterable', 'slug', 'name', 'nami_id']; public $fillable = ['is_age_group', 'is_filterable', 'slug', 'name', 'nami_id'];
/** /**
@ -26,9 +23,6 @@ class Subactivity extends Model
*/ */
public $timestamps = false; public $timestamps = false;
/**
* @var array<string, string>
*/
public $casts = [ public $casts = [
'is_age_group' => 'boolean', 'is_age_group' => 'boolean',
'is_filterable' => 'boolean', 'is_filterable' => 'boolean',
@ -47,7 +41,7 @@ class Subactivity extends Model
} }
/** /**
* @return BelongsToMany<Activity> * @return BelongsToMany<Activity, $this>
*/ */
public function activities(): BelongsToMany public function activities(): BelongsToMany
{ {

View File

@ -3,8 +3,8 @@
echo "drop database scoutrobot;" | sudo mysql echo "drop database scoutrobot;" | sudo mysql
echo "create database scoutrobot;" | sudo mysql echo "create database scoutrobot;" | sudo mysql
ssh -l stammsilva zoomyboy.de "cd /usr/share/webapps/nami_silva && docker compose exec db mysqldump -udb -p$SCOUTROBOT_DB_PASSWORD db" > db.tmp ssh -l stamm-silva zoomyboy.de "cd /usr/share/webapps/adrema_silva && docker compose exec db mysqldump -udb -p$SCOUTROBOT_DB_PASSWORD db" > db.tmp
sudo mysql scoutrobot < db.tmp sudo mysql adrema < db.tmp
rm db.tmp rm db.tmp
echo 'app(\App\Form\FormSettings::class)->fill(["registerUrl" => "http://stammsilva.test/anmeldung/{slug}/register", "clearCacheUrl" => "http://stammsilva.test/adrema/clear-cache"])->save();' | php artisan tinker echo 'app(\App\Form\FormSettings::class)->fill(["registerUrl" => "http://stammsilva.test/anmeldung/{slug}/register", "clearCacheUrl" => "http://stammsilva.test/adrema/clear-cache"])->save();' | php artisan tinker

View File

@ -80,12 +80,12 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"larastan/larastan": "^2.0", "larastan/larastan": "^3.0",
"laravel/envoy": "^2.8", "laravel/envoy": "^2.8",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"orchestra/testbench": "^9.0", "orchestra/testbench": "^9.0",
"pestphp/pest": "^3.0", "pestphp/pest": "^3.0",
"phpstan/phpstan-mockery": "^1.1" "phpstan/phpstan-mockery": "^2.0"
}, },
"config": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,

4016
composer.lock generated

File diff suppressed because it is too large Load Diff

6
config/init.php Normal file
View File

@ -0,0 +1,6 @@
<?php
return [
'email' => env('USER_EMAIL', 'admin@example.com'),
'password' => env('USER_PASSWORD', 'admin'),
];

View File

@ -0,0 +1,36 @@
<?php
namespace Database\Factories\Member;
use App\Member\BankAccount;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<BankAccount>
*/
class BankAccountFactory extends Factory
{
protected $model = BankAccount::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'bank_name' => $this->faker->name(),
'bic' => $this->faker->swiftBicNumber(),
'iban' => $this->faker->iban('DE'),
'blz' => $this->faker->name(),
'person' => $this->faker->name(),
'account_number' => $this->faker->name(),
];
}
public function inNami(int $namiId): self
{
return $this->state(['nami_id' => $namiId]);
}
}

View File

@ -93,6 +93,13 @@ class MemberFactory extends Factory
return $this->state(['nami_id' => $namiId]); return $this->state(['nami_id' => $namiId]);
} }
public function withBankAccount(BankAccountFactory $factory): self
{
return $this->afterCreating(function ($member) use ($factory) {
$member->bankAccount->update($factory->make()->toArray());
});
}
public function sameFamilyAs(Member $member): self public function sameFamilyAs(Member $member): self
{ {
return $this->state([ return $this->state([

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('bank_accounts', function (Blueprint $table) {
$table->unsignedBigInteger('member_id')->primary();
$table->unsignedBigInteger('nami_id')->nullable();
$table->string('iban')->nullable();
$table->string('bic')->nullable();
$table->string('blz')->nullable();
$table->string('bank_name')->nullable();
$table->string('person')->nullable();
$table->string('account_number')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bank_accounts');
}
};

View File

@ -16,10 +16,11 @@ class UserSeeder extends Seeder
public function run() public function run()
{ {
User::create([ User::create([
'email' => env('USER_EMAIL', 'admin@example.com'), 'email' => config('init.email'),
'email_verified_at' => now(), 'email_verified_at' => now(),
'password' => Hash::make(env('USER_PASSWORD', 'admin')), 'password' => Hash::make(config('init.password')),
'name' => 'Adrema Benutzer', 'firstname' => 'Adrema',
'lastname' => 'Benutzer',
]); ]);
} }
} }

View File

@ -2,7 +2,6 @@ version: '3'
services: services:
webservice: webservice:
image: zoomyboy/adrema-webservice:latest image: zoomyboy/adrema-webservice:latest
restart: always
depends_on: depends_on:
- php - php
ports: ports:
@ -12,7 +11,6 @@ services:
php: php:
image: zoomyboy/adrema-app:latest image: zoomyboy/adrema-app:latest
restart: always
depends_on: depends_on:
- db - db
- redis - redis
@ -38,7 +36,6 @@ services:
horizon: horizon:
image: zoomyboy/adrema-app:latest image: zoomyboy/adrema-app:latest
restart: always
depends_on: depends_on:
- php - php
command: /bin/entrypoint horizon command: /bin/entrypoint horizon
@ -62,7 +59,6 @@ services:
schedule: schedule:
image: zoomyboy/adrema-app:latest image: zoomyboy/adrema-app:latest
restart: always
depends_on: depends_on:
- php - php
command: /bin/entrypoint schedule command: /bin/entrypoint schedule
@ -86,7 +82,6 @@ services:
db: db:
image: mariadb:10.6.5 image: mariadb:10.6.5
restart: always
env_file: env_file:
- .app.env - .app.env
environment: environment:
@ -98,7 +93,6 @@ services:
socketi: socketi:
image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian
restart: always
environment: environment:
SOKETI_DEFAULT_APP_ID: adremaid SOKETI_DEFAULT_APP_ID: adremaid
SOKETI_DEFAULT_APP_KEY: adremakey SOKETI_DEFAULT_APP_KEY: adremakey
@ -106,7 +100,6 @@ services:
redis: redis:
image: redis:alpine3.18 image: redis:alpine3.18
restart: always
volumes: volumes:
- ./data/redis:/data - ./data/redis:/data

1396
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit bc61530e510b3d41048984b7cf20b6d82c4f85fb Subproject commit f905c316ee7913cbf85c386021fbaa28b4b2a158

@ -1 +1 @@
Subproject commit 74800de149bf2ca250a17263cfaf59e48b76186f Subproject commit ed283d97ca7680b3c27b2d75da9937f4f379e321

View File

@ -7,6 +7,9 @@ parameters:
stubFiles: stubFiles:
- tests/stub/phpstan/TestResponse.stub - tests/stub/phpstan/TestResponse.stub
- tests/stub/phpstan/Settings.stub - tests/stub/phpstan/Settings.stub
- tests/stub/phpstan/DataEloquentCast.stub
- tests/stub/phpstan/File.stub
- tests/stub/phpstan/CastsAttributes.stub
paths: paths:
- app - app
@ -30,9 +33,6 @@ parameters:
ignoreErrors: ignoreErrors:
- -
message: "#but does not specify its types: TData#" message: "#but does not specify its types: TData#"
-
message: "#cast\\(\\) has parameter \\$properties#"
- -
message: "#^Method App\\\\Activity\\:\\:sluggable\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Activity\\:\\:sluggable\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@ -453,11 +453,6 @@ parameters:
count: 1 count: 1
path: app/Form/Fields/NamiField.php path: app/Form/Fields/NamiField.php
-
message: "#^Method App\\\\Fileshare\\\\ConnectionTypes\\\\ConnectionType\\:\\:types\\(\\) should return Illuminate\\\\Support\\\\Collection\\<int, class\\-string\\<App\\\\Fileshare\\\\ConnectionTypes\\\\ConnectionType\\>\\> but returns Illuminate\\\\Support\\\\Collection\\<int, string\\>\\.$#"
count: 1
path: app/Fileshare/ConnectionTypes/ConnectionType.php
- -
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:check\\(\\)\\.$#" message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy\\:\\:check\\(\\)\\.$#"
count: 1 count: 1
@ -507,3 +502,45 @@ parameters:
message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\<\\*, \\*, \\*\\>\\:\\:isTrying\\(\\)\\.$#" message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\<\\*, \\*, \\*\\>\\:\\:isTrying\\(\\)\\.$#"
count: 1 count: 1
path: app/Membership/TestersBlock.php path: app/Membership/TestersBlock.php
-
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Builder\<Illuminate\\Database\\Eloquent\\Model\>\:\:isLeader\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Efz/EfzPendingBlock.php
-
message: '#^Method App\\Fileshare\\ConnectionTypes\\ConnectionType\:\:types\(\) should return Illuminate\\Support\\Collection\<int, class\-string\<App\\Fileshare\\ConnectionTypes\\ConnectionType\>\> but returns Illuminate\\Support\\Collection\<int, non\-falsy\-string\>\.$#'
identifier: return.type
count: 1
path: app/Fileshare/ConnectionTypes/ConnectionType.php
-
message: '#^Unable to resolve the template type TGroupKey in call to method Illuminate\\Support\\Collection\<int,App\\Form\\Models\\Participant\>\:\:groupBy\(\)$#'
identifier: argument.templateType
count: 1
path: app/Form/Actions/ExportSyncAction.php
-
message: '#^Parameter \#1 \$callback of method Illuminate\\Support\\Collection\<\(int\|string\),mixed\>\:\:map\(\) contains unresolvable type\.$#'
identifier: argument.unresolvableType
count: 1
path: app/Mailgateway/Resources/MailgatewayResource.php
-
message: '#^Parameter \#1 \$value of method Illuminate\\Support\\Collection\<\(int\|string\),array\<string, mixed\>\>\:\:prepend\(\) contains unresolvable type\.$#'
identifier: argument.unresolvableType
count: 1
path: app/Mailgateway/Resources/MailgatewayResource.php
-
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Builder\<Illuminate\\Database\\Eloquent\\Model\>\:\:isLeader\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Member/PsPendingBlock.php
-
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Builder\<TRelatedModel of Illuminate\\Database\\Eloquent\\Model\>\:\:isTrying\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Membership/TestersBlock.php

View File

@ -29,6 +29,7 @@
<server name="QUEUE_CONNECTION" value="sync"/> <server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/> <server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/> <server name="TELESCOPE_ENABLED" value="false"/>
<server name="SCOUT_DRIVER" value="database"/>
</php> </php>
<source> <source>
<include> <include>

View File

@ -90,8 +90,8 @@ class MassStoreActionTest extends TestCase
$this->assertDatabaseCount('invoices', 1); $this->assertDatabaseCount('invoices', 1);
$this->assertDatabaseCount('invoice_positions', 2); $this->assertDatabaseCount('invoice_positions', 2);
$this->assertDatabaseHas('invoice_positions', ['description' => 'beitrag Max Muster']); $this->assertDatabaseHas('invoice_positions', ['description' => 'beitrag Max Muster', 'price' => 4466]);
$this->assertDatabaseHas('invoice_positions', ['description' => 'beitrag Jane Muster']); $this->assertDatabaseHas('invoice_positions', ['description' => 'beitrag Jane Muster', 'price' => 4466]);
} }
public function testItSeparatesBillKinds(): void public function testItSeparatesBillKinds(): void
@ -105,4 +105,21 @@ class MassStoreActionTest extends TestCase
$this->assertDatabaseCount('invoices', 2); $this->assertDatabaseCount('invoices', 2);
$this->assertDatabaseCount('invoice_positions', 2); $this->assertDatabaseCount('invoice_positions', 2);
} }
public function testItSeparatesSubscriptions(): void
{
$member1 = Member::factory()->defaults()->emailBillKind()
->for(Subscription::factory()->forFee()->children([new Child('beitrag1 {name}', 4466)]))
->create(['firstname' => 'Member1', 'lastname' => 'ln']);
$member2 = Member::factory()->defaults()->sameFamilyAs($member1)->emailBillKind()
->for(Subscription::factory()->forFee()->children([new Child('beitrag2 {name}', 4467)]))
->create(['firstname' => 'Member2']);
$this->postJson(route('invoice.mass-store'), ['year' => now()->addYear()->year])->assertOk();
$invoice = Invoice::first();
$this->assertDatabaseCount('invoice_positions', 2);
$this->assertDatabaseHas('invoice_positions', ['invoice_id' => $invoice->id, 'member_id' => $member1->id, 'description' => 'beitrag1 Member1 ln', 'price' => 4466]);
$this->assertDatabaseHas('invoice_positions', ['invoice_id' => $invoice->id, 'member_id' => $member2->id, 'description' => 'beitrag2 Member2 ln', 'price' => 4467]);
}
} }

View File

@ -7,11 +7,11 @@ use App\Actions\PullMembershipsAction;
use App\Activity; use App\Activity;
use App\Confession; use App\Confession;
use App\Country; use App\Country;
use App\Fee; use App\Gender;
use App\Group; use App\Group;
use App\Member\Actions\NamiPutMemberAction; use App\Member\Actions\NamiPutMemberAction;
use App\Member\BankAccount;
use App\Member\Member; use App\Member\Member;
use App\Nationality;
use App\Payment\Subscription; use App\Payment\Subscription;
use App\Region; use App\Region;
use App\Subactivity; use App\Subactivity;
@ -20,52 +20,130 @@ use Phake;
use Zoomyboy\LaravelNami\Fakes\MemberFake; use Zoomyboy\LaravelNami\Fakes\MemberFake;
uses(DatabaseTransactions::class); uses(DatabaseTransactions::class);
covers(NamiPutMemberAction::class);
it('testItPutsAMember', function (array $memberAttributes, array $storedAttributes) { beforeEach(function () {
Fee::factory()->create();
$this->stubIo(PullMemberAction::class, fn ($mock) => $mock); $this->stubIo(PullMemberAction::class, fn ($mock) => $mock);
$this->stubIo(PullMembershipsAction::class, fn ($mock) => $mock); $this->stubIo(PullMembershipsAction::class, fn ($mock) => $mock);
Group::factory()->inNami(55)->create();
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$country = Country::factory()->create();
$region = Region::factory()->create();
$nationality = Nationality::factory()->inNami(565)->create();
$subscription = Subscription::factory()->forFee()->create();
$group = Group::factory()->inNami(55)->create();
$confession = Confession::factory()->inNami(567)->create(['is_null' => true]);
app(MemberFake::class)->stores(55, 993); app(MemberFake::class)->stores(55, 993);
$activity = Activity::factory()->hasAttached(Subactivity::factory()->name('Biber')->inNami(55))->name('Leiter')->inNami(6)->create(); Confession::factory()->inNami(567)->create(['is_null' => true]);
$subactivity = $activity->subactivities->first(); $activity = Activity::factory()->inNami(6)->create();
Subactivity::factory()->hasAttached($activity)->inNami(55)->create();
});
$member = Member::factory() it('pulls member and memberships befre pushing', function () {
->for($country) $member = Member::factory()->defaults()->create();
->for($subscription)
->for($region)
->for($nationality)
->for($group)
->emailBillKind()
->create($memberAttributes);
NamiPutMemberAction::run($member, $activity, $subactivity); NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
app(MemberFake::class)->assertStored(55, [
'ersteTaetigkeitId' => 6,
'ersteUntergliederungId' => 55,
'konfessionId' => 567,
...$storedAttributes,
]);
$this->assertDatabaseHas('members', [
'nami_id' => 993,
]);
Phake::verify(app(PullMemberAction::class))->handle(55, 993); Phake::verify(app(PullMemberAction::class))->handle(55, 993);
Phake::verify(app(PullMembershipsAction::class))->handle($member); Phake::verify(app(PullMembershipsAction::class))->handle($member);
});
it('sets nami id of member', function () {
$member = Member::factory()->defaults()->create();
NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
$this->assertDatabaseHas('members', ['nami_id' => 993]);
});
it('stores member attributes', function (array $memberAttributes, array $storedAttributes) {
$member = Member::factory()->defaults()->create($memberAttributes);
NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
app(MemberFake::class)->assertStored(55, $storedAttributes);
})->with([ })->with([
[ [['firstname' => 'Phi'], ['vorname' => 'Phi']],
['email_parents' => 'a@b.de'], ['emailVertretungsberechtigter' => 'a@b.de'], [['lastname' => 'Phi'], ['nachname' => 'Phi']],
], [['nickname' => 'Nick'], ['spitzname' => 'Nick']],
[ [['email' => 'a@b.de'], ['email' => 'a@b.de']],
['keepdata' => true], ['wiederverwendenFlag' => true], [['zip' => '5566'], ['plz' => '5566']],
], [['location' => 'SG'], ['ort' => 'SG']],
[ [['further_address' => 'SG'], ['nameZusatz' => 'SG']],
['keepdata' => false], ['wiederverwendenFlag' => false], [['other_country' => 'SG'], ['staatsangehoerigkeitText' => 'SG']],
], [['address' => 'Add'], ['strasse' => 'Add']],
[['main_phone' => '+49 212 5566234'], ['telefon1' => '+49 212 5566234']],
[['mobile_phone' => '+49 212 5566234'], ['telefon2' => '+49 212 5566234']],
[['work_phone' => '+49 212 5566234'], ['telefon3' => '+49 212 5566234']],
[['email_parents' => 'a@b.de'], ['emailVertretungsberechtigter' => 'a@b.de']],
[['keepdata' => true], ['wiederverwendenFlag' => true]],
[['keepdata' => false], ['wiederverwendenFlag' => false]],
fn () => [['joined_at' => now()], ['eintrittsdatum' => now()->format('Y-m-d') . ' 00:00:00']],
[['fax' => '555'], ['telefax' => '555']],
[[], ['konfessionId' => 567]],
[[], ['ersteTaetigkeitId' => 6]],
[[], ['ersteUntergliederungId' => 55]],
]); ]);
it('stores related models', function () {
Subscription::factory()->forFee(3)->create();
$member = Member::factory()->defaults()
->for(Country::factory()->inNami(1)->create())
->for(Region::factory()->inNami(2)->create())
->for(Gender::factory()->inNami(4)->create())
->create();
NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
app(MemberFake::class)->assertStored(55, ['regionId' => 2, 'landId' => 1, 'beitragsartId' => 3, 'geschlechtId' => 4]);
});
it('stores bank account with empty values', function () {
$member = Member::factory()->defaults()->create(['mitgliedsnr' => 56]);
NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
app(MemberFake::class)->assertStored(55, ['kontoverbindung' => json_encode([
'id' => '',
'zahlungsKonditionId' => null,
'mitgliedsNummer' => 56,
'institut' => '',
'kontoinhaber' => '',
'kontonummer' => '',
'bankleitzahl' => '',
'iban' => '',
'bic' => ''
])]);
});
it('updates existing member', function () {
app(MemberFake::class)->updatesSuccessfully(55, 103)->shows(55, 103);
$member = Member::factory()->defaults()->inNami(103)->create(['version' => 50]);
NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
app(MemberFake::class)->assertUpdated(55, 103, ['id' => 103, 'version' => 50]);
});
it('updates bank account with filled values', function () {
app(MemberFake::class)->updatesSuccessfully(55, 103)->shows(55, 103);
$member = Member::factory()->defaults()
->withBankAccount(BankAccount::factory()->inNami(30)->state([
'bank_name' => 'Stadt',
'bic' => 'SOLSDE33',
'iban' => 'DE50',
'blz' => 'ssss',
'person' => 'Pill',
'account_number' => 'ddf',
]))
->inNami(103)
->create(['mitgliedsnr' => 56]);
NamiPutMemberAction::run($member, Activity::first(), Subactivity::first());
app(MemberFake::class)->assertUpdated(55, 103, ['kontoverbindung' => json_encode([
'id' => 30,
'zahlungsKonditionId' => null,
'mitgliedsNummer' => 56,
'institut' => 'Stadt',
'kontoinhaber' => 'Pill',
'kontonummer' => 'ddf',
'bankleitzahl' => 'ssss',
'iban' => 'DE50',
'bic' => 'SOLSDE33'
])]);
});

View File

@ -7,6 +7,7 @@ use App\Country;
use App\Fee; use App\Fee;
use App\Gender; use App\Gender;
use App\Group; use App\Group;
use App\Member\Member;
use App\Nationality; use App\Nationality;
use App\Payment\Subscription; use App\Payment\Subscription;
use App\Region; use App\Region;
@ -87,6 +88,26 @@ it('testFetchWiederverwendenFlag', function (array $memberAttributes, array $sto
[['regionId' => 999], ['region_id' => null]] [['regionId' => 999], ['region_id' => null]]
]); ]);
it('testFetchesKontoverbindung', function (array $memberAttributes, array $storedAttributes) {
$this->loginNami();
Region::factory()->inNami(999)->name('nicht-de')->create(['is_null' => true]);
app(MemberFake::class)->shows(1000, 1001, $memberAttributes);
app(PullMemberAction::class)->handle(1000, 1001);
$this->assertDatabaseHas('bank_accounts', [
'member_id' => Member::first()->id,
...$storedAttributes
]);
})->with([
[['kontoverbindung' => ['iban' => '3300', 'bic' => 'SOLSDE']], ['iban' => '3300', 'bic' => 'SOLSDE']],
[['kontoverbindung' => ['id' => 33003]], ['nami_id' => 33003]],
[['kontoverbindung' => ['blz' => 111]], ['blz' => 111]],
[['kontoverbindung' => ['institut' => 'Sparkasse']], ['bank_name' => 'Sparkasse']],
[['kontoverbindung' => ['kontoinhaber' => 'Max']], ['person' => 'Max']],
[['kontoverbindung' => ['kontonummer' => '333']], ['account_number' => '333']],
]);
it('testItSetsFirstSubscriptionFromFee', function () { it('testItSetsFirstSubscriptionFromFee', function () {
$this->loginNami(); $this->loginNami();
Region::factory()->inNami(999)->name('nicht-de')->create(['is_null' => true]); Region::factory()->inNami(999)->name('nicht-de')->create(['is_null' => true]);
@ -142,3 +163,20 @@ it('testItPullsMemberWithNoSubscription', function () {
'subscription_id' => null, 'subscription_id' => null,
]); ]);
}); });
it('doesnt set first subscription if fee matches', function () {
$this->loginNami();
Subscription::factory()->forFee(55)->create();
$otherSubscription = Subscription::factory()->forFee(55)->create();
$member = Member::factory()->defaults()->inNami(1001)->create(['subscription_id' => $otherSubscription->id]);
app(MemberFake::class)->shows(1000, 1001, [
'beitragsartId' => 55,
]);
app(PullMemberAction::class)->handle(1000, 1001);
$this->assertDatabaseHas('members', [
'subscription_id' => $otherSubscription->id,
'id' => $member->id,
]);
});

View File

@ -35,10 +35,10 @@ class InitializeGroupsTest extends TestCase
public function testItSynchsAGroupWithASingleNodeAndNoChildren(): void public function testItSynchsAGroupWithASingleNodeAndNoChildren(): void
{ {
$parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]); $parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]);
$this->api->method('groups')->will($this->returnValueMap([ $this->api->method('groups')->willReturnMap([
[null, collect([$parentGroup])], [null, collect([$parentGroup])],
[$parentGroup, collect([])], [$parentGroup, collect([])],
])); ]);
$this->api->method('groups')->willReturn(collect([])); $this->api->method('groups')->willReturn(collect([]));
(new InitializeGroups($this->api))->handle(); (new InitializeGroups($this->api))->handle();
@ -55,10 +55,10 @@ class InitializeGroupsTest extends TestCase
{ {
$existingGroup = GroupModel::factory()->create(['nami_id' => 150, 'inner_name' => 'Def']); $existingGroup = GroupModel::factory()->create(['nami_id' => 150, 'inner_name' => 'Def']);
$parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]); $parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]);
$this->api->method('groups')->will($this->returnValueMap([ $this->api->method('groups')->willReturnMap([
[null, collect([$parentGroup])], [null, collect([$parentGroup])],
[$parentGroup, collect([])], [$parentGroup, collect([])],
])); ]);
(new InitializeGroups($this->api))->handle(); (new InitializeGroups($this->api))->handle();
@ -75,11 +75,11 @@ class InitializeGroupsTest extends TestCase
{ {
$parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]); $parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]);
$subgroup = Group::from(['id' => 200, 'name' => 'subgroup', 'parentId' => 150]); $subgroup = Group::from(['id' => 200, 'name' => 'subgroup', 'parentId' => 150]);
$this->api->method('groups')->will($this->returnValueMap([ $this->api->method('groups')->willReturnMap([
[null, collect([$parentGroup])], [null, collect([$parentGroup])],
[$parentGroup, collect([$subgroup])], [$parentGroup, collect([$subgroup])],
[$subgroup, collect([])], [$subgroup, collect([])],
])); ]);
(new InitializeGroups($this->api))->handle(); (new InitializeGroups($this->api))->handle();
@ -94,12 +94,12 @@ class InitializeGroupsTest extends TestCase
$parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]); $parentGroup = Group::from(['id' => 150, 'name' => 'lorem', 'parentId' => null]);
$subgroup = Group::from(['id' => 200, 'name' => 'subgroup', 'parentId' => 150]); $subgroup = Group::from(['id' => 200, 'name' => 'subgroup', 'parentId' => 150]);
$subsubgroup = Group::from(['id' => 250, 'name' => 'subsubgroup', 'parentId' => 200]); $subsubgroup = Group::from(['id' => 250, 'name' => 'subsubgroup', 'parentId' => 200]);
$this->api->method('groups')->will($this->returnValueMap([ $this->api->method('groups')->willReturnMap([
[null, collect([$parentGroup])], [null, collect([$parentGroup])],
[$parentGroup, collect([$subgroup])], [$parentGroup, collect([$subgroup])],
[$subgroup, collect([$subsubgroup])], [$subgroup, collect([$subsubgroup])],
[$subsubgroup, collect([])], [$subsubgroup, collect([])],
])); ]);
(new InitializeGroups($this->api))->handle(); (new InitializeGroups($this->api))->handle();
@ -111,11 +111,11 @@ class InitializeGroupsTest extends TestCase
$existingSubgroup = GroupModel::factory()->create(['name' => 'Abc', 'inner_name' => 'Def', 'nami_id' => 200]); $existingSubgroup = GroupModel::factory()->create(['name' => 'Abc', 'inner_name' => 'Def', 'nami_id' => 200]);
$parentGroup = Group::from(['id' => 150, 'name' => 'root', 'parentId' => null]); $parentGroup = Group::from(['id' => 150, 'name' => 'root', 'parentId' => null]);
$subgroup = Group::from(['id' => 200, 'name' => 'child', 'parentId' => 150]); $subgroup = Group::from(['id' => 200, 'name' => 'child', 'parentId' => 150]);
$this->api->method('groups')->will($this->returnValueMap([ $this->api->method('groups')->willReturnMap([
[null, collect([$parentGroup])], [null, collect([$parentGroup])],
[$parentGroup, collect([$subgroup])], [$parentGroup, collect([$subgroup])],
[$subgroup, collect([])], [$subgroup, collect([])],
])); ]);
(new InitializeGroups($this->api))->handle(); (new InitializeGroups($this->api))->handle();

View File

@ -0,0 +1,35 @@
<?php
namespace Illuminate\Contracts\Database\Eloquent;
use Illuminate\Database\Eloquent\Model;
/**
* @template TGet
* @template TSet
* @copy
*/
interface CastsAttributes
{
/**
* Transform the attribute from the underlying model values.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array<string, mixed> $attributes
* @return TGet|null
*/
public function get(Model $model, string $key, mixed $value, array $attributes);
/**
* Transform the attribute to its underlying model values.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param TSet|null $value
* @param array<string, mixed> $attributes
* @return mixed
*/
public function set(Model $model, string $key, mixed $value, array $attributes);
}

View File

@ -0,0 +1,13 @@
<?php
namespace Spatie\LaravelData\Support\EloquentCasts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
/**
* @template TDto
* @implements CastsAttributes<TDto, TDto>
*/
class DataEloquentCast implements CastsAttributes
{
}

View File

@ -0,0 +1,11 @@
<?php
namespace Symfony\Component\HttpFoundation\File;
/**
* @copy
*/
class File
{
}