Remove old payments

This commit is contained in:
Philipp Lang 2023-12-19 02:00:42 +01:00
parent 156b92f765
commit 2a6fd1152b
34 changed files with 150 additions and 803 deletions

View File

@ -0,0 +1,40 @@
<?php
namespace App\Invoice\Actions;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\Models\Invoice;
use App\Invoice\RememberDocument;
use Illuminate\Http\Response;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class MassPostPdfAction
{
use AsAction;
public function handle(): BaseCompiler|Response
{
$documents = [];
foreach (Invoice::whereNeedsBill()->where('via', BillKind::POST)->get() as $invoice) {
$document = BillDocument::fromInvoice($invoice);
$documents[] = $document;
$invoice->sent($document);
}
foreach (Invoice::whereNeedsRemember()->where('via', BillKind::POST)->get() as $invoice) {
$document = RememberDocument::fromInvoice($invoice);
$documents[] = $document;
$invoice->sent($document);
}
if (!count($documents)) {
return response()->noContent();
}
return Tex::merge($documents);
}
}

View File

@ -7,10 +7,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class BillDocument extends InvoiceDocument
{
public function linkLabel(): string
{
return 'Rechnung erstellen';
}
public function getSubject(): string
{
@ -21,40 +17,4 @@ class BillDocument extends InvoiceDocument
{
return 'tex.bill';
}
public static function sendAllLabel(): string
{
return 'Rechnungen versenden';
}
public function afterSingle(Payment $payment): void
{
$payment->update([
'invoice_data' => $this->toArray(),
'status_id' => 2,
]);
}
/**
* @param HasMany<Payment> $query
*
* @return HasMany<Payment>
*/
public static function paymentsQuery(HasMany $query): HasMany
{
return $query->whereNeedsBill();
}
/**
* Get Descriptions for sendpayment page.
*
* @return array<int, string>
*/
public static function getDescription(): array
{
return [
'Diese Funktion erstellt ein PDF mit allen noch nicht versendenden Rechnungen bei den Mitgliedern die Post als Versandweg haben.',
'Die Rechnungen werden automatisch auf "Rechnung gestellt" aktualisiert.',
];
}
}

View File

@ -14,21 +14,6 @@ abstract class InvoiceDocument extends Document
{
abstract public function getSubject(): string;
abstract public function view(): string;
abstract public function afterSingle(Payment $payment): void;
abstract public function linkLabel(): string;
abstract public static function sendAllLabel(): string;
/**
* @param HasMany<Payment> $query
*
* @return HasMany<Payment>
*/
abstract public static function paymentsQuery(HasMany $query): HasMany;
/**
* @return array<int, string>
*/
abstract public static function getDescription(): array;
public string $until;
public string $filename;

View File

@ -91,10 +91,11 @@ class Invoice extends Model
*/
public function scopeWhereNeedsRemember(Builder $query): Builder
{
return $query->where('status', InvoiceStatus::SENT)->whereNotNull('sent_at')->where(function ($query) {
return $query->orWhere('last_remembered_at', '<=', now()->subMonths(3))
->orWhereNull('last_remembered_at');
});
return $query
->where('status', InvoiceStatus::SENT)
->whereNotNull('sent_at')
->whereNotNull('last_remembered_at')
->where('last_remembered_at', '<=', now()->subMonths(3));
}
public function getMailRecipient(): stdClass
@ -111,6 +112,7 @@ class Invoice extends Model
$this->update([
'sent_at' => now(),
'status' => InvoiceStatus::SENT,
'last_remembered_at' => now(),
]);
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Invoice\Queries;
use App\Invoice\BillKind;
use App\Member\Member;
use Illuminate\Database\Eloquent\Builder;
class BillKindQuery extends InvoiceMemberQuery
{
public function __construct(
private BillKind $billKind
) {
}
/**
* @return Builder<Member>
*/
protected function getQuery(): Builder
{
return Member::where('bill_kind', $this->billKind);
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Invoice\Queries;
use App\Invoice\InvoiceDocument;
use App\Member\Member;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
abstract class InvoiceMemberQuery
{
/**
* @param class-string<InvoiceDocument> $type
*/
public string $type;
/**
* @return Builder<Member>
*/
abstract protected function getQuery(): Builder;
/**
* @return Collection<(int|string), EloquentCollection<(int|string), Member>>
*/
public function getMembers(): Collection
{
return $this->get()->groupBy(
fn ($member) => Str::slug(
"{$member->lastname}{$member->address}{$member->zip}{$member->location}",
),
)->toBase();
}
/**
* @param class-string<InvoiceDocument> $type
*/
public function type(string $type): self
{
$this->type = $type;
return $this;
}
/**
* @return EloquentCollection<int, Member>
*/
private function get(): EloquentCollection
{
return $this->getQuery()
->with([
'payments' => fn ($query) => $this->type::paymentsQuery($query)
->orderByRaw('nr, member_id'),
])
->get()
->filter(fn (Member $member) => $member->payments->count() > 0);
}
}

View File

@ -7,10 +7,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class RememberDocument extends InvoiceDocument
{
public function linkLabel(): string
{
return 'Erinnerung erstellen';
}
public function getSubject(): string
{
@ -21,37 +17,4 @@ class RememberDocument extends InvoiceDocument
{
return 'tex.remember';
}
public static function sendAllLabel(): string
{
return 'Erinnerungen versenden';
}
public function afterSingle(Payment $payment): void
{
$payment->update(['last_remembered_at' => now()]);
}
/**
* @param HasMany<Payment> $query
*
* @return HasMany<Payment>
*/
public static function paymentsQuery(HasMany $query): HasMany
{
return $query->whereNeedsRemember();
}
/**
* Get Descriptions for sendpayment page.
*
* @return array<int, string>
*/
public static function getDescription(): array
{
return [
'Diese Funktion erstellt Erinnerungs-PDFs mit allen versendeten aber noch nich bezahlten Rechnungen bei den Mitgliedern die Post als Versandweg haben.',
'Das zuletzt erinnerte Datum wird auf heute gesetzt.',
];
}
}

View File

@ -54,6 +54,7 @@ class InvoiceResource extends JsonResource
'links' => [
'mass-store' => route('invoice.mass-store'),
'store' => route('invoice.store'),
'masspdf' => route('invoice.masspdf'),
],
'vias' => BillKind::forSelect(),
'statuses' => InvoiceStatus::forSelect(),

View File

@ -18,13 +18,14 @@ class MemberShowAction
public function handle(Member $member): array
{
return [
'data' => new MemberResource($member
->load('memberships')
->load('payments.subscription.children')
->load('nationality')
->load('region')
->load('subscription')
->load('courses.course')
'data' => new MemberResource(
$member
->load('memberships')
->load('invoicePositions.invoice')
->load('nationality')
->load('region')
->load('subscription')
->load('courses.course')
),
'meta' => MemberResource::meta(),
];
@ -33,7 +34,7 @@ class MemberShowAction
public function asController(Member $member): Response
{
session()->put('menu', 'member');
session()->put('title', 'Mitglied '.$member->fullname);
session()->put('title', 'Mitglied ' . $member->fullname);
return Inertia::render('member/ShowView', $this->handle($member));
}

View File

@ -11,7 +11,6 @@ use App\Invoice\BillKind;
use App\Invoice\Models\InvoicePosition;
use App\Nami\HasNamiField;
use App\Nationality;
use App\Payment\Payment;
use App\Payment\Subscription;
use App\Pdf\Sender;
use App\Region;
@ -111,13 +110,6 @@ class Member extends Model implements Geolocatable
$this->update(['version' => $version]);
}
public function createPayment(array $attributes): void
{
$this->payments()->create(array_merge($attributes, [
'last_remembered_at' => now(),
]));
}
// ----------------------------------- Getters -----------------------------------
public function getFullnameAttribute(): string
{
@ -270,14 +262,6 @@ class Member extends Model implements Geolocatable
return $this->hasMany(Membership::class);
}
/**
* @return HasMany<Payment>
*/
public function payments(): HasMany
{
return $this->hasMany(Payment::class)->orderBy('nr');
}
/**
* @return HasMany<Membership>
*/
@ -297,7 +281,6 @@ class Member extends Model implements Geolocatable
public static function booted()
{
static::deleting(function (self $model): void {
$model->payments->each->delete();
$model->memberships->each->delete();
$model->courses->each->delete();
$model->invoicePositions->each(function ($position) {
@ -327,11 +310,9 @@ class Member extends Model implements Geolocatable
public function scopeWithPendingPayment(Builder $query): Builder
{
return $query->addSelect([
'pending_payment' => Payment::selectRaw('SUM(subscription_children.amount)')
->whereColumn('payments.member_id', 'members.id')
->whereNeedsPayment()
->join('subscriptions', 'subscriptions.id', 'payments.subscription_id')
->join('subscription_children', 'subscriptions.id', 'subscription_children.parent_id'),
'pending_payment' => InvoicePosition::selectRaw('SUM(price)')
->whereColumn('invoice_positions.member_id', 'members.id')
->whereHas('invoice', fn ($query) => $query->whereNeedsPayment())
]);
}
@ -352,8 +333,8 @@ class Member extends Model implements Geolocatable
*/
public function scopeWhereAusstand(Builder $query): Builder
{
return $query->whereHas('payments', function ($q) {
return $q->whereHas('status', fn ($q) => $q->where('is_remember', true));
return $query->whereHas('invoicePositions', function ($q) {
return $q->whereHas('invoice', fn ($query) => $query->whereNeedsPayment());
});
}

View File

@ -8,14 +8,13 @@ use App\Course\Models\Course;
use App\Course\Resources\CourseMemberResource;
use App\Gender;
use App\Invoice\BillKind;
use App\Invoice\Resources\InvoicePositionResource;
use App\Lib\HasMeta;
use App\Member\Data\NestedGroup;
use App\Member\Resources\NationalityResource;
use App\Member\Resources\RegionResource;
use App\Membership\MembershipResource;
use App\Nationality;
use App\Payment\PaymentResource;
use App\Payment\Status;
use App\Payment\Subscription;
use App\Payment\SubscriptionResource;
use App\Region;
@ -72,11 +71,11 @@ class MemberResource extends JsonResource
'bill_kind_name' => optional($this->bill_kind)->value,
'has_nami' => null !== $this->nami_id,
'children_phone' => $this->children_phone,
'payments' => PaymentResource::collection($this->whenLoaded('payments')),
'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.') . ' €' : null,
'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug,
'courses' => CourseMemberResource::collection($this->whenLoaded('courses')),
'memberships' => MembershipResource::collection($this->whenLoaded('memberships')),
'invoicePositions' => InvoicePositionResource::collection($this->whenLoaded('invoicePositions')),
'nationality' => new NationalityResource($this->whenLoaded('nationality')),
'region' => new RegionResource($this->whenLoaded('region')),
'full_address' => $this->fullAddress,
@ -157,7 +156,6 @@ class MemberResource extends JsonResource
'links' => [
'index' => route('member.index'),
'create' => route('member.create'),
'sendpayment' => route('sendpayment.create'),
],
];
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Payment;
use App\Invoice\DocumentFactory;
use Illuminate\Support\Collection;
class ActionFactory
{
/**
* @return Collection<int, array{link: array{href: string, label: mixed}, text: mixed}>
*/
public function allLinks(): Collection
{
return app(DocumentFactory::class)->getTypes()->map(function ($repo) {
return [
'link' => [
'href' => route('sendpayment.pdf', ['type' => $repo]),
'label' => $repo::sendAllLabel(),
],
'text' => $repo::getDescription(),
];
});
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace App\Payment;
use App\Member\Member;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Payment extends Model
{
use HasFactory;
/**
* @var array<int, string>
*/
public $fillable = ['member_id', 'invoice_data', 'subscription_id', 'nr', 'status_id', 'last_remembered_at'];
/**
* @var array<string, string>
*/
public $casts = [
'invoice_data' => 'json',
'last_remembered_at' => 'date',
];
/**
* @return BelongsTo<Member, self>
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
/**
* @return BelongsTo<Subscription, self>
*/
public function subscription(): BelongsTo
{
return $this->belongsTo(Subscription::class);
}
/**
* @return BelongsTo<Status, self>
*/
public function status(): BelongsTo
{
return $this->belongsTo(Status::class);
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNeedsPayment(Builder $query): Builder
{
return $query->whereHas('status', function ($q) {
return $q->needsPayment();
});
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNeedsBill(Builder $query): Builder
{
return $query->whereHas('status', function ($q) {
return $q->where('is_bill', true);
});
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNeedsRemember(Builder $query): Builder
{
return $query->whereHas('status', function ($q) {
return $q->where('is_remember', true);
})->where(fn ($query) => $query->whereNull('last_remembered_at')->orWhere('last_remembered_at', '<=', now()->subMonths(3)));
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Payment;
use App\Member\Member;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Payment
*/
class PaymentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*
* @return array
*/
public function toArray($request)
{
return [
'subscription_id' => $this->subscription_id,
'subscription' => new SubscriptionResource($this->whenLoaded('subscription')),
'status_name' => $this->status->name,
'status_id' => $this->status->id,
'nr' => $this->nr,
'id' => $this->id,
'is_accepted' => $this->status->isAccepted(),
'links' => [
'show' => $this->invoice_data
? route('payment.pdf', ['payment' => $this->getModel()])
: null,
]
];
}
/**
* @return array<string, mixed>
*/
public static function memberMeta(Member $member): array
{
return [
'statuses' => Status::forSelect(),
'subscriptions' => Subscription::forSelect(),
'default' => [
'nr' => '',
'subscription_id' => null,
'status_id' => null
],
'links' => [
'store' => route('member.payment.store', ['member' => $member]),
]
];
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Payment;
use App\Http\Controllers\Controller;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\Queries\BillKindQuery;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Inertia\Inertia;
use Inertia\Response as InertiaResponse;
use Zoomyboy\Tex\Tex;
class SendpaymentController extends Controller
{
public function create(): InertiaResponse
{
session()->put('menu', 'member');
session()->put('title', 'Rechnungen versenden');
return Inertia::render('sendpayment/VForm', [
'types' => app(ActionFactory::class)->allLinks(),
]);
}
/**
* @return Response|Responsable
*/
public function send(Request $request)
{
$memberCollection = (new BillKindQuery(BillKind::POST))->type($request->type)->getMembers();
if ($memberCollection->isEmpty()) {
return response()->noContent();
}
$documents = $memberCollection->map(function ($members) use ($request) {
$document = $request->type::fromMembers($members);
app(DocumentFactory::class)->afterSingle($document, $members);
return $document;
});
return Tex::merge($documents->all());
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace App\Payment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* @deprecated
*/
class Status extends Model
{
use HasFactory;
public $fillable = ['name', 'is_bill', 'is_remember'];
public $timestamps = false;
public $casts = [
'is_bill' => 'boolean',
'is_remember' => 'boolean',
];
public static function default(): int
{
return static::where('is_bill', true)->where('is_remember', true)->first()->id;
}
public function isAccepted(): bool
{
return false === $this->is_bill && false === $this->is_remember;
}
// ---------------------------------- Scopes -----------------------------------
/**
* @param Builder<self> $query
* @return Builder<self>
*/
public function scopeNeedsPayment(Builder $query): Builder
{
return $query->where(function (Builder $query): Builder {
return $query->where('is_bill', true)->orWhere('is_remember', true);
});
}
/**
* @return array<int, array{name: string, id: int}>
*/
public static function forSelect(): array
{
return static::select('name', 'id')->get()->toArray();
}
}

View File

@ -8,10 +8,8 @@ use App\Group;
use App\Invoice\BillKind;
use App\Member\Member;
use App\Nationality;
use App\Payment\Payment;
use App\Payment\Subscription;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
/**
* @extends Factory<Member>
@ -82,20 +80,6 @@ class MemberFactory extends Factory
return $this->state(['nami_id' => $namiId]);
}
/**
* @param array<int, callable> $payments
*/
public function withPayments(array $payments): self
{
return $this->afterCreating(function (Model $model) use ($payments): void {
foreach ($payments as $paymentClosure) {
$factory = Payment::factory()->for($model);
$factory = call_user_func($paymentClosure, $factory);
$factory->create();
}
});
}
public function sameFamilyAs(Member $member): self
{
return $this->state([

View File

@ -1,60 +0,0 @@
<?php
namespace Database\Factories\Payment;
use App\Fee;
use App\Payment\Payment;
use App\Payment\Status;
use App\Payment\Subscription;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
use Tests\RequestFactories\Child;
/**
* @extends Factory<Payment>
*/
class PaymentFactory extends Factory
{
protected $model = Payment::class;
public function definition(): array
{
return [
'nr' => $this->faker->year,
'subscription_id' => Subscription::factory()->create()->id,
'status_id' => Status::factory()->create()->id,
'last_remembered_at' => now(),
];
}
public function notPaid(): self
{
return $this->for(Status::whereName('Nicht bezahlt')->first());
}
public function pending(): self
{
return $this->for(Status::whereName('Rechnung gestellt')->first())->state(['last_remembered_at' => now()->subYears(2)]);;
}
public function paid(): self
{
return $this->for(Status::whereName('Rechnung beglichen')->first());
}
public function nr(string $nr): self
{
return $this->state(['nr' => $nr]);
}
/**
* @param array<int, Child> $children
* @param array<string, mixed> $state
*/
public function subscription(string $name, array $children, array $state = []): self
{
return $this->for(
Subscription::factory()->state(['name' => $name])->for(Fee::factory())->children($children)->state($state)
);
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Database\Factories\Payment;
use App\Payment\Status;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Status>
*/
class StatusFactory extends Factory
{
public $model = Status::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->sentence,
'is_bill' => $this->faker->boolean,
'is_remember' => $this->faker->boolean,
];
}
}

View File

@ -21,10 +21,6 @@ class CreatePaymentsTable extends Migration
$table->boolean('is_remember');
});
Status::create(['name' => 'Nicht bezahlt', 'is_bill' => true, 'is_remember' => true]);
Status::create(['name' => 'Rechnung gestellt', 'is_bill' => false, 'is_remember' => true]);
Status::create(['name' => 'Rechnung beglichen', 'is_bill' => false, 'is_remember' => false]);
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->string('nr');

View File

@ -92,6 +92,8 @@ services:
socketi:
image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian
ports:
- "6001:6001"
environment:
SOKETI_DEFAULT_APP_ID: adremaid
SOKETI_DEFAULT_APP_KEY: adremakey

@ -1 +1 @@
Subproject commit bbab104f7e00c059ffffa115f6b769e2333e137a
Subproject commit b4dbd7d3125aca2c16ca9f99ec81c12a46a18e3b

View File

@ -116,11 +116,6 @@ parameters:
count: 1
path: app/Member/Member.php
-
message: "#^Method App\\\\Member\\\\Member\\:\\:createPayment\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
count: 1
path: app/Member/Member.php
-
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1
@ -136,11 +131,6 @@ parameters:
count: 1
path: app/Membership/MembershipResource.php
-
message: "#^Method App\\\\Payment\\\\PaymentResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: app/Payment/PaymentResource.php
-
message: "#^Method App\\\\Payment\\\\SubscriptionResource\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@ -181,11 +171,6 @@ parameters:
count: 1
path: database/factories/NationalityFactory.php
-
message: "#^Method Database\\\\Factories\\\\Payment\\\\StatusFactory\\:\\:definition\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: database/factories/Payment/StatusFactory.php
-
message: "#^Call to an undefined method Phake\\\\Proxies\\\\StubberProxy.*#"

View File

@ -4,7 +4,10 @@
<page-toolbar-button color="primary" icon="plus" @click="create">Rechnung anlegen</page-toolbar-button>
<page-toolbar-button color="primary" icon="plus" @click="massstore = { year: '' }">Massenrechnung
anlegen</page-toolbar-button>
<page-toolbar-button :href="meta.links.masspdf" color="primary" icon="plus">Post-Briefe
abrufen</page-toolbar-button>
</template>
<ui-popup v-if="massstore !== null" heading="Massenrechnung anlegen" @close="massstore = null">
<form @submit.prevent="sendMassstore">
<section class="grid grid-cols-2 gap-3 mt-6">

View File

@ -5,8 +5,6 @@
anlegen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary"
icon="invoice">Rechnungen erstellen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.sendpayment" color="info"
icon="envelope">Rechnungen versenden</page-toolbar-button>
</template>
<ui-popup v-if="deleting !== null" heading="Mitglied löschen?" @close="deleting.reject()">
<div>

View File

@ -30,6 +30,7 @@ use App\Invoice\Actions\DisplayRememberpdfAction;
use App\Invoice\Actions\InvoiceDestroyAction;
use App\Invoice\Actions\InvoiceIndexAction;
use App\Invoice\Actions\InvoiceUpdateAction;
use App\Invoice\Actions\MassPostPdfAction;
use App\Invoice\Actions\MassStoreAction;
use App\Invoice\Actions\PaymentPositionIndexAction;
use App\Maildispatcher\Actions\CreateAction;
@ -52,7 +53,6 @@ use App\Membership\Actions\MembershipDestroyAction;
use App\Membership\Actions\MembershipStoreAction;
use App\Membership\Actions\MembershipUpdateAction;
use App\Membership\Actions\StoreForGroupAction;
use App\Payment\SendpaymentController;
use App\Payment\SubscriptionController;
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
@ -70,8 +70,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::delete('/member/{member}', MemberDeleteAction::class);
Route::get('/member/{member}', MemberShowAction::class)->name('member.show');
Route::resource('subscription', SubscriptionController::class);
Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create');
Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf');
Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz');
Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync');
Route::get('member-export', ExportAction::class)->name('member-export');
@ -114,6 +112,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::delete('/invoice/{invoice}', InvoiceDestroyAction::class)->name('invoice.destroy');
Route::get('/invoice/{invoice}/pdf', DisplayPdfAction::class)->name('invoice.pdf');
Route::get('/invoice/{invoice}/rememberpdf', DisplayRememberpdfAction::class)->name('invoice.rememberpdf');
Route::get('/invoice/masspdf', MassPostPdfAction::class)->name('invoice.masspdf');
// ----------------------------- invoice-position ------------------------------

View File

@ -3,6 +3,8 @@
namespace Tests\EndToEnd;
use App\Group;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use App\Payment\Payment;
use Illuminate\Foundation\Testing\DatabaseMigrations;
@ -57,9 +59,7 @@ class MemberIndexTest extends TestCase
$this->withoutExceptionHandling()->login()->loginNami();
$group = Group::factory()->create();
Member::factory()->defaults()->for($group)
->has(Payment::factory()->notPaid()->subscription('tollerbeitrag', [
new Child('a', 5400),
]))
->has(InvoicePosition::factory()->for(Invoice::factory()))
->create(['firstname' => '::firstname::']);
Member::factory()->defaults()->for($group)->create(['firstname' => '::firstname::']);

View File

@ -1,44 +0,0 @@
<?php
namespace Tests\Feature\Invoice;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\Invoice;
use App\Invoice\Queries\BillKindQuery;
use App\Invoice\Queries\InvoiceMemberQuery;
use App\Member\Member;
use App\Payment\Payment;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class BillRememberDocumentTest extends TestCase
{
use DatabaseTransactions;
public function testItCreatesOneFileForFamilyMembers(): void
{
Member::factory()
->defaults()
->postBillKind()
->state(['firstname' => 'Max1', 'lastname' => '::lastname::', 'address' => '::address::', 'zip' => '12345', 'location' => '::location::'])
->has(Payment::factory()->notPaid()->nr('nr1'))
->create();
Member::factory()
->defaults()
->postBillKind()
->state(['firstname' => 'Max2', 'lastname' => '::lastname::', 'address' => '::address::', 'zip' => '12345', 'location' => '::location::'])
->has(Payment::factory()->notPaid()->nr('nr2'))
->create();
$this->assertCount(2, $this->query(BillDocument::class)->getMembers()->first());
}
/**
* @param class-string<Invoice> $type
*/
private function query(string $type): InvoiceMemberQuery
{
return (new BillKindQuery(BillKind::POST))->type($type);
}
}

View File

@ -48,6 +48,7 @@ class InvoiceIndexActionTest extends TestCase
->assertInertiaPath('data.data.0.links.destroy', route('invoice.destroy', ['invoice' => $invoice]))
->assertInertiaPath('data.meta.links.mass-store', route('invoice.mass-store'))
->assertInertiaPath('data.meta.links.store', route('invoice.store'))
->assertInertiaPath('data.meta.links.masspdf', route('invoice.masspdf'))
->assertInertiaPath('data.meta.vias.0', ['id' => 'E-Mail', 'name' => 'E-Mail'])
->assertInertiaPath('data.meta.statuses.0', ['id' => 'Neu', 'name' => 'Neu'])
->assertInertiaPath('data.meta.members.0', ['id' => $member->id, 'name' => 'Aaaa Aaab'])

View File

@ -52,7 +52,7 @@ class InvoiceSendActionTest extends TestCase
->has(InvoicePosition::factory()->withMember(), 'positions')
->via(BillKind::EMAIL)
->status(InvoiceStatus::SENT)
->create(['sent_at' => now()->subMonths(6), 'mail_email' => 'max@muster.de']);
->create(['sent_at' => now()->subMonths(6), 'mail_email' => 'max@muster.de', 'last_remembered_at' => now()->subMonths(6)]);
InvoiceSendAction::run();

View File

@ -0,0 +1,61 @@
<?php
namespace Tests\Feature\Invoice;
use App\Invoice\BillKind;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MassPostPdfActionTest extends TestCase
{
use DatabaseTransactions;
public function testItDoesntDisplayPdfWhenNoMembersFound(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$this->get(route('invoice.masspdf'))->assertStatus(204);
}
public function testItDoesntDisplayPdfWhenAllInvoicesPaid(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->via(BillKind::POST)->status(InvoiceStatus::PAID)->create();
$this->get(route('invoice.masspdf'))->assertStatus(204);
}
public function testItDoesntDisplayEmailBills(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->via(BillKind::EMAIL)->create();
$this->get(route('invoice.masspdf'))->assertStatus(204);
}
public function testItMergesRememberAndBill(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$invoice1 = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->status(InvoiceStatus::NEW)
->via(BillKind::POST)
->create();
$invoice2 = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->status(InvoiceStatus::SENT)
->via(BillKind::POST)
->create(['sent_at' => now()->subMonths(10), 'last_remembered_at' => now()->subMonths(4)]);
$this->get(route('invoice.masspdf'))->assertPdfPageCount(2);
$this->assertEquals(InvoiceStatus::SENT, $invoice1->fresh()->status);
$this->assertEquals(now()->format('Y-m-d'), $invoice1->fresh()->last_remembered_at->format('Y-m-d'));
$this->assertEquals(now()->format('Y-m-d'), $invoice1->fresh()->sent_at->format('Y-m-d'));
$this->assertEquals(InvoiceStatus::SENT, $invoice2->fresh()->status);
$this->assertEquals(now()->format('Y-m-d'), $invoice2->fresh()->last_remembered_at->format('Y-m-d'));
$this->assertEquals(now()->subMonths(10)->format('Y-m-d'), $invoice2->fresh()->sent_at->format('Y-m-d'));
}
}

View File

@ -4,6 +4,9 @@ namespace Tests\Feature\Member;
use App\Activity;
use App\Group;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use App\Member\Membership;
use App\Payment\Payment;
@ -175,7 +178,7 @@ class IndexTest extends TestCase
{
$this->withoutExceptionHandling()->login()->loginNami();
Member::factory()
->has(Payment::factory()->notPaid()->subscription('Free', [new Child('b', 50)]))
->has(InvoicePosition::factory()->for(Invoice::factory()->status(InvoiceStatus::NEW)))
->defaults()->create();
Member::factory()->defaults()->create();
Member::factory()->defaults()->create();

View File

@ -7,6 +7,8 @@ use App\Course\Models\CourseMember;
use App\Fee;
use App\Gender;
use App\Group;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use App\Member\Membership;
use App\Nationality;
@ -32,10 +34,7 @@ class ShowTest extends TestCase
->defaults()
->for(Group::factory()->name('Stamm Beispiel'))
->has(Membership::factory()->promise(now())->in('€ LeiterIn', 5, 'Jungpfadfinder', 88)->from('2022-11-19'))
->has(Payment::factory()->notPaid()->nr('2019')->subscription('Free', [
new Child('uu', 1000),
new Child('a', 50),
]))
->has(InvoicePosition::factory()->for(Invoice::factory())->price(1050)->description('uu'))
->for(Gender::factory()->name('Männlich'))
->for(Region::factory()->name('NRW'))
->postBillKind()
@ -130,15 +129,12 @@ class ShowTest extends TestCase
],
], $response, 'data.courses.0');
$this->assertInertiaHas([
'subscription' => [
'name' => 'Free',
'id' => $member->payments->first()->subscription->id,
'amount' => 1050,
'amount_human' => '10,50 €',
],
'status_name' => 'Nicht bezahlt',
'nr' => '2019',
], $response, 'data.payments.0');
'description' => 'uu',
'price_human' => '10,50 €',
'invoice' => [
'status' => 'Neu',
]
], $response, 'data.invoicePositions.0');
}
public function testItShowsMinimalSingleMember(): void

View File

@ -1,129 +0,0 @@
<?php
namespace Tests\Feature\Sendpayment;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\InvoiceSettings;
use App\Invoice\Queries\BillKindQuery;
use App\Invoice\RememberDocument;
use App\Member\Member;
use App\Payment\Payment;
use App\Payment\Status;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child;
use Tests\RequestFactories\InvoiceSettingsFake;
use Tests\TestCase;
use Zoomyboy\Tex\Tex;
class SendpaymentTest extends TestCase
{
use DatabaseTransactions;
public function testItCanViewSendpaymentPage(): void
{
$this->withoutExceptionHandling();
$this->login()->loginNami();
$response = $this->get(route('sendpayment.create'));
$response->assertOk();
$this->assertInertiaHas('Rechnungen versenden', $response, 'types.0.link.label');
$href = $this->inertia($response, 'types.0.link.href');
$this->assertStringContainsString('BillDocument', $href);
}
public function testItDownloadsPdfOfAllMembersForBill(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
$this->withoutExceptionHandling()->login()->loginNami();
Member::factory()->defaults()->postBillKind()->count(3)
->has(Payment::factory()->notPaid()->subscription('tollerbeitrag', [new Child('a', 5400)]))
->create();
$response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\BillDocument']);
$response->assertOk();
$this->assertPdfPageCount(3, $response->getFile());
}
public function testItDownloadsPdfOfAllMembersForRemember(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
$this->withoutExceptionHandling()->login()->loginNami();
Member::factory()->defaults()->postBillKind()->count(3)
->has(Payment::factory()->pending()->subscription('tollerbeitrag', [new Child('a', 5400)]))
->create();
$response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\RememberDocument']);
$response->assertOk();
$this->assertPdfPageCount(3, $response->getFile());
}
public function testItCanCreatePdfPayments(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
Tex::spy();
$this->withoutExceptionHandling()->login()->loginNami();
$members = Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [new Child('a', 5400)]))
->has(Payment::factory()->paid()->nr('1998')->subscription('bezahltdesc', [new Child('b', 5800)]))
->postBillKind()
->count(3)
->create();
$member = $members->first();
$this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\BillDocument']);
$this->assertEquals(Status::firstWhere('name', 'Rechnung gestellt')->id, $member->payments->firstWhere('nr', '1997')->status_id);
$this->assertEquals(Status::firstWhere('name', 'Rechnung beglichen')->id, $member->payments->firstWhere('nr', '1998')->status_id);
Tex::assertCompiled(
BillDocument::class,
fn ($document) => $document->hasAllContent(['1997', 'tollerbeitrag', '54.00'])
&& $document->missesAllContent(['1998', 'bezahltdesc', '58.00'])
);
$member->payments->firstWhere('nr', '1997')->update(['status_id' => Status::firstWhere('name', 'Nicht bezahlt')->id]);
$invoice = BillDocument::fromMembers((new BillKindQuery(BillKind::POST))->type(BillDocument::class)->getMembers()->first());
$this->assertEquals(
BillDocument::from($member->payments->firstWhere('nr', '1997')->invoice_data)->renderBody(),
$invoice->renderBody()
);
}
public function testItCanCreatePdfPaymentsForRemember(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
Tex::spy();
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Payment::factory()->pending()->nr('1997')->subscription('tollerbeitrag', [new Child('a', 5400)]))
->postBillKind()
->create();
$this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\RememberDocument']);
Tex::assertCompiled(
RememberDocument::class,
fn ($document) => $document->hasAllContent(['1997', 'tollerbeitrag', '54.00'])
);
$this->assertNull($member->payments()->first()->invoice_data);
$this->assertEquals(now()->format('Y-m-d'), $member->payments->first()->last_remembered_at->format('Y-m-d'));
}
public function testItDoesntCreatePdfsWhenUserHasEmail(): void
{
Tex::spy();
$this->withoutExceptionHandling();
$this->login()->loginNami();
Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [new Child('u', 5400)]))
->emailBillKind()
->create();
$response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\BillDocument']);
$response->assertStatus(204);
Tex::assertNotCompiled(BillDocument::class);
}
}