Compare commits

...

17 Commits

Author SHA1 Message Date
Philipp Lang 5d5b08ed78 Remove ports for socketi
continuous-integration/drone/push Build is failing Details
2023-12-19 02:19:18 +01:00
Philipp Lang 3697885bca Fix views 2023-12-19 02:18:58 +01:00
Philipp Lang 2a6fd1152b Remove old payments 2023-12-19 02:00:42 +01:00
Philipp Lang 156b92f765 Add Remember mail for sending 2023-12-18 02:17:31 +01:00
Philipp Lang afbfdf7ca2 Remove mail_name 2023-12-18 01:56:58 +01:00
Philipp Lang be29a284d5 Fix Send invoices 2023-12-18 01:15:16 +01:00
Philipp Lang 68a654494d Add display remember pdf 2023-12-18 00:16:58 +01:00
Philipp Lang f5ad0d46fd Mod Stub 2023-12-18 00:06:04 +01:00
Philipp Lang 8d0d05463f Mod usage in MassStoreAction 2023-12-17 23:29:01 +01:00
Philipp Lang 19dea7a061 Add link to invoice PDF to frontend 2023-12-17 23:27:55 +01:00
Philipp Lang f4dc8b24bc Add usage to invoice 2023-12-17 23:00:52 +01:00
Philipp Lang b0534279b6 Add DisplayPdfAction for invoices 2023-12-17 22:33:29 +01:00
Philipp Lang 551c658fa3 Lint 2023-12-17 21:59:51 +01:00
Philipp Lang 07a0c22a69 Remove old actions 2023-12-17 21:24:16 +01:00
Philipp Lang 2e8c41d5d9 Mod InvoicePosition in member overview 2023-12-17 21:13:52 +01:00
Philipp Lang 703c74a9f4 Lint 2023-12-17 02:03:39 +01:00
Philipp Lang bf8f6c87a8 Fix MemberPaymentsBlock 2023-12-17 01:49:12 +01:00
79 changed files with 982 additions and 1848 deletions

View File

@ -4,10 +4,10 @@ namespace App\Dashboard;
use App\Dashboard\Blocks\Block;
use App\Efz\EfzPendingBlock;
use App\Invoice\MemberPaymentBlock;
use App\Member\PsPendingBlock;
use App\Membership\AgeGroupCountBlock;
use App\Membership\TestersBlock;
use App\Payment\MemberPaymentBlock;
class DashboardFactory
{

View File

@ -0,0 +1,20 @@
<?php
namespace App\Invoice\Actions;
use App\Invoice\BillDocument;
use App\Invoice\Models\Invoice;
use Illuminate\Http\Response;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class DisplayPdfAction
{
use AsAction;
public function handle(Invoice $invoice): BaseCompiler|Response
{
return Tex::compile(BillDocument::fromInvoice($invoice));
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Invoice\Actions;
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 DisplayRememberpdfAction
{
use AsAction;
public function handle(Invoice $invoice): BaseCompiler|Response
{
return Tex::compile(RememberDocument::fromInvoice($invoice));
}
}

View File

@ -16,6 +16,8 @@ trait HasValidation
return [
'status' => ['required', 'string', 'max:255', Rule::in(InvoiceStatus::values())],
'via' => ['required', 'string', 'max:255', Rule::in(BillKind::values())],
'usage' => 'required|max:255|string',
'mail_email' => 'nullable|string|max:255|email',
'to' => 'array',
'to.address' => 'required|string|max:255',
'to.location' => 'required|string|max:255',
@ -42,6 +44,7 @@ trait HasValidation
'to.location' => 'Ort',
'status' => 'Status',
'via' => 'Rechnungsweg',
'usage' => 'Verwendungszweck',
];
}
}

View File

@ -2,11 +2,13 @@
namespace App\Invoice\Actions;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\Queries\BillKindQuery;
use App\Payment\Payment;
use App\Payment\PaymentMail;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Mails\BillMail;
use App\Invoice\Mails\RememberMail;
use App\Invoice\Models\Invoice;
use App\Invoice\RememberDocument;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Lorisleiva\Actions\Concerns\AsAction;
@ -33,15 +35,18 @@ class InvoiceSendAction
*/
public function handle(): int
{
foreach (app(DocumentFactory::class)->getTypes() as $type) {
$memberCollection = (new BillKindQuery(BillKind::EMAIL))->type($type)->getMembers();
foreach ($memberCollection as $members) {
$invoice = $type::fromMembers($members);
$invoicePath = Storage::disk('temp')->path(Tex::compile($invoice)->storeIn('', 'temp'));
Mail::to($invoice->getRecipient())->send(new PaymentMail($invoice, $invoicePath));
app(DocumentFactory::class)->afterSingle($invoice, $members);
foreach (Invoice::whereNeedsBill()->where('via', BillKind::EMAIL)->get() as $invoice) {
$document = BillDocument::fromInvoice($invoice);
$path = Storage::disk('temp')->path(Tex::compile($document)->storeIn('', 'temp'));
Mail::to($invoice->getMailRecipient())->send(new BillMail($invoice, $path));
$invoice->sent($document);
}
foreach (Invoice::whereNeedsRemember()->where('via', BillKind::EMAIL)->get() as $invoice) {
$document = RememberDocument::fromInvoice($invoice);
$path = Storage::disk('temp')->path(Tex::compile($document)->storeIn('', 'temp'));
Mail::to($invoice->getMailRecipient())->send(new RememberMail($invoice, $path));
$invoice->sent($document);
}
return 0;

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

@ -0,0 +1,31 @@
<?php
namespace App\Invoice\Actions;
use App\Invoice\Models\InvoicePosition;
use App\Invoice\Resources\InvoicePositionResource;
use App\Member\Member;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class PaymentPositionIndexAction
{
use AsAction;
/**
* @return Collection<int, InvoicePosition>
*/
public function handle(Member $member): Collection
{
return $member->load('invoicePositions.invoice')->invoicePositions;
}
public function asController(Member $member): JsonResponse
{
return response()->json([
'data' => InvoicePositionResource::collection($this->handle($member)),
]);
}
}

View File

@ -5,12 +5,8 @@ namespace App\Invoice;
use App\Payment\Payment;
use Illuminate\Database\Eloquent\Relations\HasMany;
class BillDocument extends Invoice
class BillDocument extends InvoiceDocument
{
public function linkLabel(): string
{
return 'Rechnung erstellen';
}
public function getSubject(): string
{
@ -21,45 +17,4 @@ class BillDocument extends Invoice
{
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,
]);
}
public function getMailSubject(): string
{
return 'Jahresrechnung';
}
/**
* @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

@ -1,37 +0,0 @@
<?php
namespace App\Invoice;
use App\Member\Member;
use Illuminate\Support\Collection;
class DocumentFactory
{
/**
* @var array<int, class-string<Invoice>>
*/
private array $types = [
BillDocument::class,
RememberDocument::class,
];
/**
* @return Collection<int, class-string<Invoice>>
*/
public function getTypes(): Collection
{
return collect($this->types);
}
/**
* @param Collection<(int|string), Member> $members
*/
public function afterSingle(Invoice $invoice, Collection $members): void
{
foreach ($members as $member) {
foreach ($member->payments as $payment) {
$invoice->afterSingle($payment);
}
}
}
}

View File

@ -1,145 +0,0 @@
<?php
namespace App\Invoice;
use App\Member\Member;
use App\Payment\Payment;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Document;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
abstract class Invoice 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;
/**
* @param array<string, string> $positions
*/
public function __construct(
public string $familyName,
public string $singleName,
public string $address,
public string $zip,
public string $location,
public array $positions,
public string $usage,
public ?string $email,
) {
$this->until = now()->addWeeks(2)->format('d.m.Y');
$this->filename = Str::slug("{$this->getSubject()} für {$familyName}");
}
/**
* @param Collection<(int|string), Member> $members
*/
public static function fromMembers(Collection $members): self
{
return static::withoutMagicalCreationFrom([
'familyName' => $members->first()->lastname,
'singleName' => $members->first()->lastname,
'address' => $members->first()->address,
'zip' => $members->first()->zip,
'location' => $members->first()->location,
'email' => $members->first()->email_parents ?: $members->first()->email,
'positions' => static::renderPositions($members),
'usage' => "Mitgliedsbeitrag für {$members->first()->lastname}",
]);
}
public function settings(): InvoiceSettings
{
return app(InvoiceSettings::class);
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public function basename(): string
{
return $this->filename;
}
public function template(): Template
{
return Template::make('tex.templates.default');
}
public function setFilename(string $filename): self
{
$this->filename = $filename;
return $this;
}
public function getRecipient(): MailRecipient
{
throw_unless($this->email, Exception::class, 'Cannot get Recipient. Mail not set.');
return new MailRecipient($this->email, $this->familyName);
}
/**
* @return view-string
*/
public function mailView(): string
{
$view = 'mail.payment.' . Str::snake(class_basename($this));
throw_unless(view()->exists($view), Exception::class, 'Mail view ' . $view . ' existiert nicht.');
return $view;
}
/**
* @param Collection<(int|string), Member> $members
*
* @return array<string, string>
*/
public static function renderPositions(Collection $members): array
{
/** @var array<string, string> */
$result = [];
foreach ($members->pluck('payments')->flatten(1) as $payment) {
if ($payment->subscription->split) {
foreach ($payment->subscription->children as $child) {
$result["{$payment->subscription->name} ({$child->name}) {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = static::number($child->amount);
}
} else {
$result["{$payment->subscription->name} {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = static::number($payment->subscription->getAmount());
}
}
return $result;
}
public static function number(int $number): string
{
return number_format($number / 100, 2, '.', '');
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Invoice;
use App\Invoice\Models\Invoice;
use App\Payment\Payment;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Document;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
abstract class InvoiceDocument extends Document
{
abstract public function getSubject(): string;
abstract public function view(): string;
public string $until;
public string $filename;
/**
* @param array<string, string> $positions
*/
public function __construct(
public string $toName,
public string $toAddress,
public string $toZip,
public string $toLocation,
public string $greeting,
public array $positions,
public string $usage,
) {
$this->until = now()->addWeeks(2)->format('d.m.Y');
$this->filename = Str::slug("{$this->getSubject()} für {$toName}");
}
public static function fromInvoice(Invoice $invoice): self
{
return static::withoutMagicalCreationFrom([
'toName' => $invoice->to['name'],
'toAddress' => $invoice->to['address'],
'toZip' => $invoice->to['zip'],
'toLocation' => $invoice->to['location'],
'greeting' => $invoice->greeting,
'positions' => static::renderPositions($invoice),
'usage' => $invoice->usage,
]);
}
public function settings(): InvoiceSettings
{
return app(InvoiceSettings::class);
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public function basename(): string
{
return $this->filename;
}
public function template(): Template
{
return Template::make('tex.templates.default');
}
public function setFilename(string $filename): self
{
$this->filename = $filename;
return $this;
}
/**
* @return array<string, string>
*/
public static function renderPositions(Invoice $invoice): array
{
return $invoice->positions->mapWithKeys(fn ($position) => [$position->description => static::number($position->price)])->toArray();
}
public static function number(int $number): string
{
return number_format($number / 100, 2, '.', '');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Invoice\Mails;
use App\Invoice\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class BillMail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(public Invoice $invoice, public string $filename)
{
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('mail.invoice.bill')
->attach($this->filename)
->replyTo('kasse@stamm-silva.de')
->subject('Rechnung | DPSG Stamm Silva');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Invoice\Mails;
use App\Invoice\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class RememberMail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(public Invoice $invoice, public string $filename)
{
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown('mail.invoice.remember')
->attach($this->filename)
->replyTo('kasse@stamm-silva.de')
->subject('Zahlungserinnerung | DPSG Stamm Silva');
}
}

View File

@ -1,8 +1,9 @@
<?php
namespace App\Payment;
namespace App\Invoice;
use App\Dashboard\Blocks\Block;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
class MemberPaymentBlock extends Block
@ -12,17 +13,15 @@ class MemberPaymentBlock extends Block
*/
public function data(): array
{
$amount = Payment::whereNeedsPayment()
->selectRaw('sum(subscription_children.amount) AS nr')
->join('subscriptions', 'subscriptions.id', 'payments.subscription_id')
->join('subscription_children', 'subscriptions.id', 'subscription_children.parent_id')
$amount = InvoicePosition::whereHas('invoice', fn ($query) => $query->whereNeedsPayment())
->selectRaw('sum(price) AS price')
->first();
$members = Member::whereHasPendingPayment()->count();
return [
'members' => $members,
'total_members' => Member::count(),
'amount' => number_format((int) $amount->nr / 100, 2, ',', '.').' €',
'amount' => number_format((int) $amount->price / 100, 2, ',', '.') . ' €',
];
}

View File

@ -2,12 +2,17 @@
namespace App\Invoice\Models;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\InvoiceDocument;
use App\Invoice\RememberDocument;
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\HasMany;
use stdClass;
class Invoice extends Model
{
@ -24,6 +29,7 @@ class Invoice extends Model
/** @var array<int, string> */
public $dates = [
'sent_at',
'last_remembered_at',
];
/**
@ -46,6 +52,8 @@ class Invoice extends Model
'greeting' => 'Liebe Familie ' . $member->lastname,
'status' => InvoiceStatus::NEW,
'via' => $member->bill_kind,
'usage' => 'Mitgliedsbeitrag für ' . $member->lastname,
'mail_email' => $member->email_parents ?: $member->email,
]);
}
@ -55,4 +63,64 @@ class Invoice extends Model
$model->positions()->delete();
});
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNeedsPayment(Builder $query): Builder
{
return $query->whereIn('status', [InvoiceStatus::NEW->value, InvoiceStatus::SENT->value]);
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNeedsBill(Builder $query): Builder
{
return $query->where('status', InvoiceStatus::NEW);
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNeedsRemember(Builder $query): Builder
{
return $query
->where('status', InvoiceStatus::SENT)
->whereNotNull('sent_at')
->whereNotNull('last_remembered_at')
->where('last_remembered_at', '<=', now()->subMonths(3));
}
public function getMailRecipient(): stdClass
{
return (object) [
'email' => $this->mail_email,
'name' => $this->to['name']
];
}
public function sent(InvoiceDocument $document): void
{
if (is_a($document, BillDocument::class)) {
$this->update([
'sent_at' => now(),
'status' => InvoiceStatus::SENT,
'last_remembered_at' => now(),
]);
}
if (is_a($document, RememberDocument::class)) {
$this->update([
'last_remembered_at' => now(),
'status' => InvoiceStatus::SENT,
]);
}
}
}

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\Invoice;
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<Invoice> $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<Invoice> $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

@ -5,12 +5,8 @@ namespace App\Invoice;
use App\Payment\Payment;
use Illuminate\Database\Eloquent\Relations\HasMany;
class RememberDocument extends Invoice
class RememberDocument extends InvoiceDocument
{
public function linkLabel(): string
{
return 'Erinnerung erstellen';
}
public function getSubject(): string
{
@ -21,42 +17,4 @@ class RememberDocument extends Invoice
{
return 'tex.remember';
}
public static function sendAllLabel(): string
{
return 'Erinnerungen versenden';
}
public function afterSingle(Payment $payment): void
{
$payment->update(['last_remembered_at' => now()]);
}
public function getMailSubject(): string
{
return 'Zahlungserinnerung';
}
/**
* @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

@ -24,6 +24,8 @@ class InvoicePositionResource extends JsonResource
'price' => $this->price,
'member_id' => $this->member_id,
'description' => $this->description,
'invoice' => new InvoiceResource($this->whenLoaded('invoice')),
'price_human' => number_format($this->price / 100, 2, ',', '') . ' €',
];
}
}

View File

@ -34,7 +34,11 @@ class InvoiceResource extends JsonResource
'via' => $this->via->value,
'positions' => InvoicePositionResource::collection($this->whenLoaded('positions')),
'greeting' => $this->greeting,
'usage' => $this->usage,
'mail_email' => $this->mail_email,
'links' => [
'pdf' => route('invoice.pdf', ['invoice' => $this->getModel()]),
'rememberpdf' => route('invoice.rememberpdf', ['invoice' => $this->getModel()]),
'update' => route('invoice.update', ['invoice' => $this->getModel()]),
'destroy' => route('invoice.destroy', ['invoice' => $this->getModel()]),
]
@ -50,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(),
@ -65,6 +70,8 @@ class InvoiceResource extends JsonResource
'greeting' => '',
'status' => InvoiceStatus::NEW->value,
'via' => null,
'usage' => '',
'mail_email' => '',
],
'default_position' => [
'id' => null,

View File

@ -18,9 +18,10 @@ class MemberShowAction
public function handle(Member $member): array
{
return [
'data' => new MemberResource($member
'data' => new MemberResource(
$member
->load('memberships')
->load('payments.subscription.children')
->load('invoicePositions.invoice')
->load('nationality')
->load('region')
->load('subscription')
@ -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())
]);
}
@ -342,9 +323,7 @@ class Member extends Model implements Geolocatable
*/
public function scopeWhereHasPendingPayment(Builder $query): Builder
{
return $query->whereHas('payments', function (Builder $q): void {
$q->whereNeedsPayment();
});
return $query->whereHas('invoicePositions', fn ($q) => $q->whereHas('invoice', fn ($q) => $q->whereNeedsPayment()));
}
/**
@ -354,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,
@ -109,7 +108,7 @@ class MemberResource extends JsonResource
'group_name' => $this->group->name,
'links' => [
'membership_index' => route('member.membership.index', ['member' => $this->getModel()]),
'payment_index' => route('member.payment.index', ['member' => $this->getModel()]),
'invoiceposition_index' => route('member.invoice-position.index', ['member' => $this->getModel()]),
'course_index' => route('member.course.index', ['member' => $this->getModel()]),
'show' => route('member.show', ['member' => $this->getModel()]),
'edit' => route('member.edit', ['member' => $this->getModel()]),
@ -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,26 +0,0 @@
<?php
namespace App\Payment\Actions;
use App\Invoice\BillDocument;
use App\Payment\Payment;
use Illuminate\Http\Response;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class DisplayPdfAction
{
use AsAction;
public function handle(Payment $payment): BaseCompiler|Response
{
if (null === $payment->invoice_data) {
return response()->noContent();
}
$invoice = BillDocument::from($payment->invoice_data);
return Tex::compile($invoice);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Payment\Actions;
use App\Member\Member;
use App\Payment\Payment;
use App\Payment\PaymentResource;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class IndexAction
{
use AsAction;
/**
* @return Collection<int, Payment>
*/
public function handle(Member $member): Collection
{
return $member->payments()->with('subscription')->get();
}
public function asController(Member $member): AnonymousResourceCollection
{
return PaymentResource::collection($this->handle($member))
->additional([
'meta' => PaymentResource::memberMeta($member),
]);
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Payment\Actions;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Payment\Payment;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class PaymentDestroyAction
{
use AsAction;
use TracksJob;
public function handle(int $paymentId): void
{
Payment::find($paymentId)->delete();
}
public function asController(Payment $payment): JsonResponse
{
$this->startJob($payment->id, $payment->member->fullname);
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$memberName = $parameters[1];
return $jobState
->before('Zahlung für ' . $memberName . ' wird gelöscht')
->after('Zahlung für ' . $memberName . ' gelöscht')
->failed('Fehler beim Löschen der Zahlung für ' . $memberName)
->shouldReload(JobChannels::make()->add('member')->add('payment'));
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Payment\Actions;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Member\Member;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class PaymentStoreAction
{
use AsAction;
use TracksJob;
/**
* @param array<string, string> $attributes
*/
public function handle(Member $member, array $attributes): void
{
$member->createPayment($attributes);
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'nr' => 'required',
'subscription_id' => 'required|exists:subscriptions,id',
'status_id' => 'required|exists:statuses,id',
];
}
public function asController(Member $member, ActionRequest $request): JsonResponse
{
$this->startJob($member, $request->validated());
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0];
return $jobState
->before('Zahlung für ' . $member->fullname . ' wird gespeichert')
->after('Zahlung für ' . $member->fullname . ' gespeichert')
->failed('Fehler beim Erstellen der Zahlung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('payment'));
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Payment\Actions;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Payment\Payment;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class PaymentUpdateAction
{
use AsAction;
use TracksJob;
/**
* @param array<string, string> $attributes
*/
public function handle(Payment $payment, array $attributes): void
{
$payment->update($attributes);
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'nr' => 'required',
'subscription_id' => 'required|exists:subscriptions,id',
'status_id' => 'required|exists:statuses,id',
];
}
public function asController(Payment $payment, ActionRequest $request): JsonResponse
{
$this->startJob($payment, $request->validated());
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0]->member;
return $jobState
->before('Zahlung für ' . $member->fullname . ' wird aktualisiert')
->after('Zahlung für ' . $member->fullname . ' aktualisiert')
->failed('Fehler beim Aktualisieren der Zahlung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('payment'));
}
}

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,43 +0,0 @@
<?php
namespace App\Payment;
use App\Invoice\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PaymentMail extends Mailable
{
use Queueable;
use SerializesModels;
public Invoice $invoice;
public string $filename;
public string $salutation;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(Invoice $invoice, string $filename)
{
$this->invoice = $invoice;
$this->filename = $filename;
$this->salutation = 'Liebe Familie ' . $invoice->familyName;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->markdown($this->invoice->mailView())
->attach($this->filename)
->replyTo('kasse@stamm-silva.de')
->subject($this->invoice->getSubject() . ' | DPSG Stamm Silva');
}
}

View File

@ -1,58 +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,
'update' => route('payment.update', ['payment' => $this->getModel()]),
'destroy' => route('payment.destroy', ['payment' => $this->getModel()]),
]
];
}
/**
* @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

@ -27,7 +27,9 @@ class InvoiceFactory extends Factory
'greeting' => $this->faker->words(4, true),
'to' => ReceiverRequestFactory::new()->create(),
'status' => InvoiceStatus::NEW->value,
'via' => BillKind::POST->value
'via' => BillKind::POST->value,
'usage' => $this->faker->words(4, true),
'mail_email' => $this->faker->safeEmail(),
];
}

View File

@ -22,13 +22,22 @@ class InvoicePositionFactory extends Factory
{
return [
'description' => $this->faker->words(4, true),
'member_id' => Member::factory()->defaults()->create()->id,
'price' => $this->faker->numberBetween(1000, 2000),
];
}
public function description(string $description): self
{
return $this->state(['description' => $description]);
}
public function price(int $price): self
{
return $this->state(['price' => $price]);
}
public function withMember(): self
{
return $this->state(['member_id' => Member::factory()->defaults()->create()->id]);
}
}

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

@ -20,6 +20,9 @@ return new class extends Migration
$table->string('status');
$table->date('sent_at')->nullable();
$table->string('via');
$table->string('usage');
$table->string('mail_email')->nullable();
$table->datetime('last_remembered_at')->nullable();
$table->timestamps();
});

@ -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.*#"
@ -492,11 +477,6 @@ parameters:
count: 1
path: app/Contribution/ContributionFactory.php
-
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Invoice\\\\Invoice\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
count: 1
path: app/Payment/ActionFactory.php
-
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Setting\\\\LocalSettings\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
count: 1

View File

@ -1,18 +1,40 @@
<template>
<button class="btn label" v-bind="$attrs" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" v-if="$attrs.onClick">
<button v-if="$attrs.onClick" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" class="btn label" v-bind="$attrs" :class="colors[color]">
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
<span class="hidden xl:inline"><slot></slot></span>
<span class="hidden xl:inline">
<slot></slot>
</span>
</button>
<i-link :href="href" class="btn label" v-bind="$attrs" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" v-else>
<i-link v-if="!$attrs.onClick && !asA" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" :href="href" class="btn label" v-bind="$attrs" :class="colors[color]">
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
<span class="hidden xl:inline"><slot></slot></span>
<span class="hidden xl:inline">
<slot></slot>
</span>
</i-link>
<a v-if="asA" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" :href="href" target="_BLANK" class="btn label" v-bind="$attrs" :class="colors[color]">
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
<span class="hidden xl:inline">
<slot></slot>
</span>
</a>
</template>
<script>
import {menuStore} from '../../stores/menuStore.js';
export default {
props: {
asA: {
type: Boolean,
default: () => false,
},
href: {
required: false,
default: () => '#',
},
icon: {},
color: {},
},
data: function () {
return {
menuStore: menuStore(),
@ -23,19 +45,11 @@ export default {
},
};
},
props: {
href: {
required: false,
default: () => '#',
},
icon: {},
color: {},
},
computed: {
slotContent() {
return this.$slots.default()[0].children;
}
},
},
};
</script>

View File

@ -2,9 +2,10 @@
<page-layout>
<template #toolbar>
<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 color="primary" icon="plus" @click="massstore = {year: ''}">Massenrechnung anlegen</page-toolbar-button>
<page-toolbar-button :href="meta.links.masspdf" color="primary" icon="plus" as-a>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">
@ -20,46 +21,43 @@
<div>
<p class="mt-4">Diese Rechnung löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="text-center btn btn-danger" @click.prevent="
<a
href="#"
class="text-center btn btn-danger"
@click.prevent="
remove(deleting);
deleting = null;
">Rechnung löschen</a>
"
>Rechnung löschen</a
>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
</div>
</div>
</ui-popup>
<ui-popup v-if="single !== null" :heading="`Rechnung ${single.id ? 'bearbeiten' : 'erstellen'}`"
inner-width="max-w-4xl" @close="cancel">
<ui-popup v-if="single !== null" :heading="`Rechnung ${single.id ? 'bearbeiten' : 'erstellen'}`" inner-width="max-w-4xl" @close="cancel">
<form class="grid grid-cols-2 gap-3 mt-4" @submit.prevent="submit">
<ui-box heading="Empfänger" container-class="grid grid-cols-2 gap-3">
<f-text id="to_name" v-model="single.to.name" name="to_name" label="Name" class="col-span-full"
required></f-text>
<f-text id="to_address" v-model="single.to.address" name="to_address" class="col-span-full"
label="Adresse" required></f-text>
<ui-box heading="Empfänger" container-class="grid grid-cols-2 gap-3 col-span-full">
<f-text id="to_name" v-model="single.to.name" name="to_name" label="Name" class="col-span-full" required></f-text>
<f-text id="to_address" v-model="single.to.address" name="to_address" label="Adresse" class="col-span-full" required></f-text>
<f-text id="to_zip" v-model="single.to.zip" name="to_zip" label="PLZ" required></f-text>
<f-text id="to_location" v-model="single.to.location" name="to_location" label="Ort" required></f-text>
<f-text id="mail_email" v-model="single.mail_email" name="mail_email" label="E-Mail-Adresse" class="col-span-full"></f-text>
</ui-box>
<ui-box heading="Status" container-class="grid gap-3">
<f-select id="status" v-model="single.status" :options="meta.statuses" name="status" label="Status"
required></f-select>
<f-select id="via" v-model="single.via" :options="meta.vias" name="via" label="Rechnungsweg"
required></f-select>
<f-select id="status" v-model="single.status" :options="meta.statuses" name="status" label="Status" required></f-select>
<f-select id="via" v-model="single.via" :options="meta.vias" name="via" label="Rechnungsweg" required></f-select>
<f-text id="greeting" v-model="single.greeting" name="greeting" label="Anrede" required></f-text>
<f-text id="usage" v-model="single.usage" name="usage" label="Verwendungszweck" required></f-text>
</ui-box>
<ui-box heading="Positionen" class="col-span-full" container-class="grid gap-3">
<template #in-title>
<ui-icon-button class="ml-3 btn-primary" icon="plus"
@click="single.positions.push({ ...meta.default_position })">Neu</ui-icon-button>
<ui-icon-button class="ml-3 btn-primary" icon="plus" @click="single.positions.push({...meta.default_position})">Neu</ui-icon-button>
</template>
<div v-for="(position, index) in single.positions" :key="index" class="flex items-end space-x-3">
<f-text :id="`position-description-${index}`" v-model="position.description" class="grow"
:name="`position-description-${index}`" label="Beschreibung" required></f-text>
<f-text :id="`position-price-${index}`" v-model="position.price" mode="area"
:name="`position-price-${index}`" label="Preis" required></f-text>
<f-select :id="`position-member-${index}`" v-model="position.member_id" :options="meta.members"
:name="`position-member-${index}`" label="Mitglied" required></f-select>
<button type="button" class="btn btn-danger btn-sm h-[35px]" icon="trash"
@click="single.positions.splice(index, 1)"><ui-sprite src="trash"></ui-sprite></button>
<f-text :id="`position-description-${index}`" v-model="position.description" class="grow" :name="`position-description-${index}`" label="Beschreibung" required></f-text>
<f-text :id="`position-price-${index}`" v-model="position.price" mode="area" :name="`position-price-${index}`" label="Preis" required></f-text>
<f-select :id="`position-member-${index}`" v-model="position.member_id" :options="meta.members" :name="`position-member-${index}`" label="Mitglied" required></f-select>
<button type="button" class="btn btn-danger btn-sm h-[35px]" icon="trash" @click="single.positions.splice(index, 1)"><ui-sprite src="trash"></ui-sprite></button>
</div>
</ui-box>
<section class="flex mt-4 space-x-2">
@ -95,10 +93,10 @@
<div v-text="invoice.via"></div>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm"
@click.prevent="edit(invoice)"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm"
@click.prevent="deleting = invoice"><ui-sprite src="trash"></ui-sprite></a>
<a v-tooltip="`Anschauen`" :href="invoice.links.pdf" target="_BLANK" class="inline-flex btn btn-info btn-sm"><ui-sprite src="eye"></ui-sprite></a>
<a v-tooltip="`Erinnerung anschauen`" :href="invoice.links.rememberpdf" target="_BLANK" class="ml-2 inline-flex btn btn-info btn-sm"><ui-sprite src="document"></ui-sprite></a>
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="edit(invoice)"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = invoice"><ui-sprite src="trash"></ui-sprite></a>
</td>
</tr>
</table>
@ -109,10 +107,10 @@
</template>
<script setup>
import { ref } from 'vue';
import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
import {ref} from 'vue';
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
const props = defineProps(indexProps);
var { axios, meta, data, reloadPage, create, single, edit, cancel, submit, remove } = useIndex(props.data, 'invoice');
var {axios, meta, data, reloadPage, create, single, edit, cancel, submit, remove} = useIndex(props.data, 'invoice');
const massstore = ref(null);
const deleting = ref(null);

View File

@ -0,0 +1,35 @@
<template>
<page-header title="Zahlungen" @close="$emit('close')"> </page-header>
<div class="grow">
<table class="custom-table custom-table-light custom-table-sm text-sm">
<thead>
<th>Beschreibung</th>
<th>Status</th>
<th>Beitrag</th>
</thead>
<tr v-for="(position, index) in data" :key="index">
<td v-text="position.description"></td>
<td v-text="position.invoice.status"></td>
<td v-text="position.price_human"></td>
</tr>
</table>
</div>
</template>
<script setup>
defineEmits(['close']);
import { useApiIndex } from '../../composables/useApiIndex.js';
const props = defineProps({
url: {
type: String,
required: true,
},
});
const { data, reload } = useApiIndex(props.url, 'payment');
await reload();
</script>

View File

@ -1,68 +0,0 @@
<template>
<page-header title="Zahlungen" @close="$emit('close')">
<template #toolbar>
<page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue
Zahlung</page-toolbar-button>
<page-toolbar-button v-if="single !== null" color="primary" icon="undo"
@click.prevent="cancel">Zurück</page-toolbar-button>
</template>
</page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-text id="nr" v-model="single.nr" label="Jahr" required></f-text>
<f-select id="subscription_id" v-model="single.subscription_id" name="subscription_id" :options="meta.subscriptions"
label="Beitrag" required></f-select>
<f-select id="status_id" v-model="single.status_id" name="status_id" :options="meta.statuses" label="Status"
required></f-select>
<button type="submit" class="btn btn-primary">Absenden</button>
</form>
<div v-else class="grow">
<table class="custom-table custom-table-light custom-table-sm text-sm">
<thead>
<th>Nr</th>
<th>Status</th>
<th>Beitrag</th>
<th></th>
</thead>
<tr v-for="(payment, index) in data" :key="index">
<td v-text="payment.nr"></td>
<td v-text="payment.status_name"></td>
<td v-text="payment.subscription.name"></td>
<td class="flex">
<a v-show="payment.links.show" :href="payment.links.show"
class="inline-flex btn btn-success btn-sm"><ui-sprite src="eye"></ui-sprite></a>
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(payment)"><ui-sprite
src="pencil"></ui-sprite></a>
<button v-show="!payment.is_accepted" class="inline-flex btn btn-success btn-sm"
@click.prevent="accept(payment)"><ui-sprite src="check"></ui-sprite></button>
<button class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(payment)"><ui-sprite
src="trash"></ui-sprite></button>
</td>
</tr>
</table>
</div>
</template>
<script setup>
defineEmits(['close']);
import { useApiIndex } from '../../composables/useApiIndex.js';
const props = defineProps({
url: {
type: String,
required: true,
},
});
const { axios, data, meta, reload, cancel, single, create, edit, submit, remove } = useApiIndex(props.url, 'payment');
async function accept(payment) {
await axios.patch(payment.links.update, { ...payment, status_id: 3 });
await reload();
}
await reload();
</script>

View File

@ -45,7 +45,7 @@
</ui-box>
<ui-box heading="Zahlungen" class="area-payments">
<payments :value="inner.payments"></payments>
<payments :value="inner.invoicePositions"></payments>
</ui-box>
<ui-box heading="Karte" container-class="grow" class="area-map hidden xl:flex">
@ -59,7 +59,6 @@
import {defineAsyncComponent} from 'vue';
export default {
props: {
data: {},
meta: {},

View File

@ -1,17 +1,21 @@
<template>
<page-layout page-class="pb-6">
<template #toolbar>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied 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>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied
anlegen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary"
icon="invoice">Rechnungen erstellen</page-toolbar-button>
</template>
<ui-popup v-if="deleting !== null" heading="Mitglied löschen?" @close="deleting.reject()">
<div>
<p class="mt-4">Das Mitglied "{{ deleting.member.fullname }}" löschen?</p>
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls entfernt.</p>
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls
entfernt.</p>
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi
vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
<ui-note v-if="deleting.member.has_nami" class="mt-5" type="danger">
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern "Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern
"Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
</ui-note>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve">Mitglied loschen</a>
@ -20,45 +24,22 @@
</div>
</ui-popup>
<page-filter breakpoint="xl">
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text>
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände" size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
<f-multipleselect
id="group_ids"
:options="meta.groups"
:model-value="getFilter('group_ids')"
label="Gruppierungen"
size="sm"
name="group_ids"
@update:model-value="setFilter('group_ids', $event)"
></f-multipleselect>
<f-select
v-show="hasModule('bill')"
id="billKinds"
name="billKinds"
:options="meta.billKinds"
:model-value="getFilter('bill_kind')"
label="Rechnung"
size="sm"
@update:model-value="setFilter('bill_kind', $event)"
></f-select>
<f-multipleselect
id="activity_ids"
:options="meta.filterActivities"
:model-value="getFilter('activity_ids')"
label="Tätigkeiten"
size="sm"
name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"
></f-multipleselect>
<f-multipleselect
id="subactivity_ids"
:options="meta.filterSubactivities"
:model-value="getFilter('subactivity_ids')"
label="Untertätigkeiten"
size="sm"
name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"
></f-multipleselect>
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
@update:model-value="setFilter('search', $event)"></f-text>
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände"
size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
<f-multipleselect id="group_ids" :options="meta.groups" :model-value="getFilter('group_ids')"
label="Gruppierungen" size="sm" name="group_ids"
@update:model-value="setFilter('group_ids', $event)"></f-multipleselect>
<f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds"
:model-value="getFilter('bill_kind')" label="Rechnung" size="sm"
@update:model-value="setFilter('bill_kind', $event)"></f-select>
<f-multipleselect id="activity_ids" :options="meta.filterActivities" :model-value="getFilter('activity_ids')"
label="Tätigkeiten" size="sm" name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"></f-multipleselect>
<f-multipleselect id="subactivity_ids" :options="meta.filterSubactivities"
:model-value="getFilter('subactivity_ids')" label="Untertätigkeiten" size="sm" name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"></f-multipleselect>
<button class="btn btn-primary label mr-2" @click.prevent="exportMembers">
<ui-sprite class="w-3 h-3 xl:mr-2" src="save"></ui-sprite>
<span class="hidden xl:inline">Exportieren</span>
@ -107,11 +88,14 @@
<div class="text-xs text-gray-200" v-text="member.full_address"></div>
<div class="flex items-center mt-1 space-x-4">
<tags :member="member"></tags>
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback=""></ui-label>
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment"
fallback=""></ui-label>
</div>
<actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"> </actions>
<actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)">
</actions>
<div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron"
class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
</div>
</ui-box>
</div>
@ -121,27 +105,30 @@
</div>
<ui-sidebar v-if="single !== null" @close="closeSidebar">
<member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index" @close="closeSidebar"></member-payments>
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar"></member-memberships>
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar"></member-courses>
<member-invoice-positions v-if="single.type === 'invoicePosition'"
:url="single.model.links.invoiceposition_index" @close="closeSidebar"></member-invoice-positions>
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index"
@close="closeSidebar"></member-memberships>
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index"
@close="closeSidebar"></member-courses>
</ui-sidebar>
</page-layout>
</template>
<script setup>
import MemberPayments from './MemberPayments.vue';
import MemberInvoicePositions from './MemberInvoicePositions.vue';
import MemberMemberships from './MemberMemberships.vue';
import MemberCourses from './MemberCourses.vue';
import Tags from './Tags.vue';
import Actions from './index/Actions.vue';
import {indexProps, useIndex} from '../../composables/useIndex.js';
import {ref, defineProps} from 'vue';
import { indexProps, useIndex } from '../../composables/useIndex.js';
import { ref, defineProps } from 'vue';
const single = ref(null);
const deleting = ref(null);
const props = defineProps(indexProps);
var {router, data, meta, getFilter, setFilter, filterString, reloadPage} = useIndex(props.data, 'member');
var { router, data, meta, getFilter, setFilter, filterString, reloadPage } = useIndex(props.data, 'member');
function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`);
@ -149,7 +136,7 @@ function exportMembers() {
async function remove(member) {
new Promise((resolve, reject) => {
deleting.value = {resolve, reject, member};
deleting.value = { resolve, reject, member };
})
.then(() => {
router.delete(`/member/${member.id}`);

View File

@ -2,21 +2,21 @@
<div>
<table cellspacing="0" cellpadding="0" border="0" class="hidden md:table custom-table custom-table-sm text-sm">
<thead>
<th>Nr</th>
<th>Beschreibung</th>
<th>Beitrag</th>
<th>Status</th>
</thead>
<tr v-for="(payment, index) in inner" :key="index">
<td v-text="payment.nr"></td>
<td v-text="`${payment.subscription.name} (${payment.subscription.amount_human})`"></td>
<td v-text="payment.status_name"></td>
<tr v-for="(position, index) in inner" :key="index">
<td v-text="position.description"></td>
<td v-text="position.price_human"></td>
<td v-text="position.invoice.status"></td>
</tr>
</table>
<div class="md:hidden grid gap-3">
<ui-box class="relative" :heading="payment.nr" v-for="(payment, index) in inner" :key="index" second>
<div class="text-xs text-gray-200" v-text="`${payment.subscription.name} (${payment.subscription.amount_human})`"></div>
<div class="text-xs text-gray-200" v-text="payment.status_name"></div>
<ui-box v-for="(position, index) in inner" :key="index" class="relative" :heading="position.description" second>
<div class="text-xs text-gray-200" v-text="position.price_human"></div>
<div class="text-xs text-gray-200" v-text="position.invoice.status"></div>
</ui-box>
</div>
</div>
@ -24,16 +24,15 @@
<script>
export default {
props: {
value: {},
},
data: function () {
return {
inner: [],
};
},
props: {
value: {},
},
created() {
this.inner = this.value;
},

View File

@ -1,14 +1,19 @@
<template>
<div class="flex space-x-1">
<i-link v-tooltip="`Details`" :href="member.links.show" class="inline-flex btn btn-primary btn-sm"><ui-sprite src="eye"></ui-sprite></i-link>
<i-link v-tooltip="`Bearbeiten`" :href="member.links.edit" class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<a v-show="hasModule('bill')" v-tooltip="`Zahlungen`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'payment')"><ui-sprite src="money"></ui-sprite></a>
<a v-show="hasModule('courses')" v-tooltip="`Ausbildungen`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'courses')"
><ui-sprite src="course"></ui-sprite
></a>
<a v-tooltip="`Mitgliedschaften`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'membership')"><ui-sprite src="user"></ui-sprite></a>
<a v-show="member.efz_link" v-tooltip="`EFZ Formular`" :href="member.efz_link" class="inline-flex btn btn-info btn-sm"><ui-sprite src="report"></ui-sprite></a>
<a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="$emit('remove')"><ui-sprite src="trash"></ui-sprite></a>
<i-link v-tooltip="`Details`" :href="member.links.show" class="inline-flex btn btn-primary btn-sm"><ui-sprite
src="eye"></ui-sprite></i-link>
<i-link v-tooltip="`Bearbeiten`" :href="member.links.edit" class="inline-flex btn btn-warning btn-sm"><ui-sprite
src="pencil"></ui-sprite></i-link>
<a v-show="hasModule('bill')" v-tooltip="`Zahlungen`" href="#" class="inline-flex btn btn-info btn-sm"
@click.prevent="$emit('sidebar', 'invoicePosition')"><ui-sprite src="money"></ui-sprite></a>
<a v-show="hasModule('courses')" v-tooltip="`Ausbildungen`" href="#" class="inline-flex btn btn-info btn-sm"
@click.prevent="$emit('sidebar', 'courses')"><ui-sprite src="course"></ui-sprite></a>
<a v-tooltip="`Mitgliedschaften`" href="#" class="inline-flex btn btn-info btn-sm"
@click.prevent="$emit('sidebar', 'membership')"><ui-sprite src="user"></ui-sprite></a>
<a v-show="member.efz_link" v-tooltip="`EFZ Formular`" :href="member.efz_link"
class="inline-flex btn btn-info btn-sm"><ui-sprite src="report"></ui-sprite></a>
<a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm"
@click.prevent="$emit('remove')"><ui-sprite src="trash"></ui-sprite></a>
</div>
</template>
@ -17,6 +22,6 @@ defineProps({
member: {
type: Object,
required: true,
}
},
});
</script>

View File

@ -1,5 +1,5 @@
@component('mail::message')
# {{ $salutation }},
# {{ $invoice->greeting }},
Im Anhang findet ihr die aktuelle Rechnung des Stammes Silva für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.

View File

@ -1,5 +1,5 @@
@component('mail::message')
# {{ $salutation }},
# {{ $invoice->greeting }},
Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge des Stammes Silva für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.

View File

@ -11,10 +11,10 @@
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
\begin{document}
\begin{letter}{Familie <<< $familyName >>>\\<<< $address >>>\\<<< $zip >>> <<< $location >>>}
\begin{letter}{<<< $toName >>>\\<<< $toAddress >>>\\<<< $toZip >>> <<< $toLocation >>>}
\sffamily
\gdef\TotalHT{0}
\opening{Liebe Familie <<< $familyName >>>,}
\opening{<<< $greeting >>>,}
Hiermit stellen wir Ihnen den aktuellen Mitgliedsbeitrag für den \usekomavar*{fromname} und die DPSG in Rechnung. Dieser setzt sich wie folgt zusammen:

View File

@ -11,10 +11,10 @@
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
\begin{document}
\begin{letter}{Familie <<< $familyName >>>\\<<< $address >>>\\<<< $zip >>> <<< $location >>>}
\begin{letter}{<<< $toName >>>\\<<< $toAddress >>>\\<<< $toZip >>> <<< $toLocation >>>}
\sffamily
\gdef\TotalHT{0}
\opening{Liebe Familie <<< $familyName >>>,}
\opening{<<< $greeting >>>,}
Ihr Mitgliedbeitrag ist noch ausstehend. Dieser setzt sich wie folgt zusammen:

View File

@ -25,10 +25,14 @@ use App\Initialize\Actions\InitializeFormAction;
use App\Initialize\Actions\NamiGetSearchLayerAction;
use App\Initialize\Actions\NamiLoginCheckAction;
use App\Initialize\Actions\NamiSearchAction;
use App\Invoice\Actions\DisplayPdfAction;
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;
use App\Maildispatcher\Actions\DestroyAction;
use App\Maildispatcher\Actions\EditAction;
@ -49,12 +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\Actions\DisplayPdfAction;
use App\Payment\Actions\IndexAction as PaymentIndexAction;
use App\Payment\Actions\PaymentDestroyAction;
use App\Payment\Actions\PaymentStoreAction;
use App\Payment\Actions\PaymentUpdateAction;
use App\Payment\SendpaymentController;
use App\Payment\SubscriptionController;
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
@ -72,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');
@ -106,13 +102,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
// ----------------------------------- group -----------------------------------
Route::get('/group', ListAction::class)->name('group.index');
// ---------------------------------- payment ----------------------------------
Route::get('/member/{member}/payment', PaymentIndexAction::class)->name('member.payment.index');
Route::get('/payment/{payment}/pdf', DisplayPdfAction::class)->name('payment.pdf');
Route::post('/member/{member}/payment', PaymentStoreAction::class)->name('member.payment.store');
Route::patch('/payment/{payment}', PaymentUpdateAction::class)->name('payment.update');
Route::delete('/payment/{payment}', PaymentDestroyAction::class)->name('payment.destroy');
// -------------------------------- allpayment ---------------------------------
Route::post('/invoice/mass-store', MassStoreAction::class)->name('invoice.mass-store');
@ -121,6 +110,13 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store');
Route::patch('/invoice/{invoice}', InvoiceUpdateAction::class)->name('invoice.update');
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 ------------------------------
Route::get('/member/{member}/invoice-position', PaymentPositionIndexAction::class)->name('member.invoice-position.index');
// --------------------------------- membership --------------------------------
Route::get('/member/{member}/membership', MembershipIndexAction::class)->name('member.membership.index');

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,171 +0,0 @@
<?php
namespace Tests\Feature\Invoice;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\Invoice;
use App\Invoice\InvoiceSettings;
use App\Invoice\Queries\BillKindQuery;
use App\Invoice\Queries\InvoiceMemberQuery;
use App\Invoice\RememberDocument;
use App\Member\Member;
use App\Payment\Payment;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child;
use Tests\TestCase;
class BillRememberDocumentTest extends TestCase
{
use DatabaseTransactions;
public function testItDisplaysMemberInformation(): void
{
$member = Member::factory()
->defaults()
->state([
'firstname' => '::firstname::',
'lastname' => '::lastname::',
'address' => '::street::',
'zip' => '::zip::',
'location' => '::location::',
])
->postBillKind()
->has(Payment::factory()->notPaid()->nr('1995')->subscription('::subName::', [
new Child('a', 1000),
new Child('a', 500),
]))
->create();
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$invoice->assertHasAllContent([
'Rechnung',
'15.00',
'::subName:: 1995 für ::firstname:: ::lastname::',
'Mitgliedsbeitrag für ::lastname::',
'Familie ::lastname::\\\\::street::\\\\::zip:: ::location::',
]);
}
public function testItDisplaysSplitPayments(): void
{
$member = Member::factory()
->defaults()
->postBillKind()
->state([
'firstname' => '::firstname::',
'lastname' => '::lastname::',
])
->has(Payment::factory()->notPaid()->nr('1995')->subscription('::subName::', [
new Child('a', 1000),
new Child('b', 500),
], ['split' => true]))
->create();
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$invoice->assertHasAllContent([
'Rechnung',
'10.00',
'5.00',
'::subName:: (a) 1995 für ::firstname:: ::lastname::',
'::subName:: (b) 1995 für ::firstname:: ::lastname::',
'Mitgliedsbeitrag für ::lastname::',
]);
}
public function testBillSetsFilename(): void
{
Member::factory()
->defaults()
->postBillKind()
->state(['lastname' => '::lastname::'])
->has(Payment::factory()->notPaid()->nr('1995'))
->create();
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$this->assertEquals('rechnung-fur-lastname.pdf', $invoice->compiledFilename());
}
public function testRememberSetsFilename(): void
{
Member::factory()
->postBillKind()
->defaults()
->state(['lastname' => '::lastname::'])
->has(Payment::factory()->notPaid()->state(['last_remembered_at' => now()->subMonths(6)]))
->create();
$invoice = RememberDocument::fromMembers($this->query(RememberDocument::class)->getMembers()->first());
$this->assertEquals('zahlungserinnerung-fur-lastname.pdf', $invoice->compiledFilename());
}
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());
}
/**
* @testWith ["App\\Invoice\\BillDocument"]
* ["App\\Invoice\\RememberDocument"]
*/
public function testItDisplaysSettings(string $type): void
{
InvoiceSettings::fake([
'from_long' => 'langer Stammesname',
'from' => 'Stammeskurz',
'mobile' => '+49 176 55555',
'email' => 'max@muster.de',
'website' => 'https://example.com',
'address' => 'Musterstr 4',
'place' => 'Münster',
'zip' => '12345',
'iban' => 'DE444',
'bic' => 'SOLSSSSS',
]);
Member::factory()
->defaults()
->postBillKind()
->has(Payment::factory()->notPaid()->nr('nr2')->state(['last_remembered_at' => now()->subYear()]))
->create();
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$invoice->assertHasAllContent([
'langer Stammesname',
'Stammeskurz',
'+49 176 55555',
'max@muster.de',
'https://example.com',
'Musterstr 4',
'Münster',
'12345',
'DE444',
'SOLSSSSS',
]);
}
/**
* @param class-string<Invoice> $type
*/
private function query(string $type): InvoiceMemberQuery
{
return (new BillKindQuery(BillKind::POST))->type($type);
}
}

View File

@ -15,7 +15,7 @@ class InvoiceDestroyActionTest extends TestCase
public function testItDestroysInvoice(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$invoice = Invoice::factory()->has(InvoicePosition::factory(), 'positions')->create();
$invoice = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->create();
$this->delete(route('invoice.destroy', ['invoice' => $invoice]))->assertOk();
$this->assertDatabaseCount('invoices', 0);

View File

@ -21,12 +21,12 @@ class InvoiceIndexActionTest extends TestCase
$member = Member::factory()->defaults()->create(['firstname' => 'Aaaa', 'lastname' => 'Aaab']);
$invoice = Invoice::factory()
->has(InvoicePosition::factory()->price(1100)->for($member)->state(['description' => 'lala']), 'positions')
->has(InvoicePosition::factory()->price(2200), 'positions')
->has(InvoicePosition::factory()->price(2200)->withMember(), 'positions')
->to(ReceiverRequestFactory::new()->name('Familie Blabla'))
->sentAt(now()->subDay())
->via(BillKind::POST)
->status(InvoiceStatus::SENT)
->create();
->create(['usage' => 'Usa', 'mail_email' => 'a@b.de']);
$this->get(route('invoice.index'))
->assertInertiaPath('data.data.0.to.name', 'Familie Blabla')
@ -35,15 +35,20 @@ class InvoiceIndexActionTest extends TestCase
->assertInertiaPath('data.data.0.sent_at_human', now()->subDay()->format('d.m.Y'))
->assertInertiaPath('data.data.0.status', 'Rechnung gestellt')
->assertInertiaPath('data.data.0.via', 'Post')
->assertInertiaPath('data.data.0.mail_email', 'a@b.de')
->assertInertiaPath('data.data.0.usage', 'Usa')
->assertInertiaPath('data.data.0.greeting', $invoice->greeting)
->assertInertiaPath('data.data.0.positions.0.price', 1100)
->assertInertiaPath('data.data.0.positions.0.member_id', $member->id)
->assertInertiaPath('data.data.0.positions.0.description', 'lala')
->assertInertiaPath('data.data.0.positions.0.id', $invoice->positions->first()->id)
->assertInertiaPath('data.data.0.links.pdf', route('invoice.pdf', ['invoice' => $invoice]))
->assertInertiaPath('data.data.0.links.rememberpdf', route('invoice.rememberpdf', ['invoice' => $invoice]))
->assertInertiaPath('data.data.0.links.update', route('invoice.update', ['invoice' => $invoice]))
->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'])
@ -58,6 +63,8 @@ class InvoiceIndexActionTest extends TestCase
'greeting' => '',
'status' => InvoiceStatus::NEW->value,
'via' => null,
'usage' => '',
'mail_email' => '',
])
->assertInertiaPath('data.meta.default_position', [
'id' => null,

View File

@ -18,7 +18,8 @@ class InvoiceRequestFactory extends RequestFactory
'greeting' => 'Hallo Familie',
'status' => InvoiceStatus::NEW->value,
'via' => BillKind::EMAIL->value,
'positions' => []
'positions' => [],
'usage' => $this->faker->words(4, true),
];
}

View File

@ -4,13 +4,16 @@ namespace Tests\Feature\Invoice;
use App\Invoice\Actions\InvoiceSendAction;
use App\Invoice\BillDocument;
use App\Member\Member;
use App\Payment\Payment;
use App\Payment\PaymentMail;
use App\Invoice\BillKind;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Mails\BillMail;
use App\Invoice\Mails\RememberMail;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Invoice\RememberDocument;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Tests\RequestFactories\Child;
use Tests\TestCase;
use Zoomyboy\Tex\Tex;
@ -18,29 +21,72 @@ class InvoiceSendActionTest extends TestCase
{
use DatabaseTransactions;
public function testItCanCreatePdfPayments(): void
public function testItSendsInvoices(): void
{
Mail::fake();
Tex::spy();
Storage::fake('temp');
$this->withoutExceptionHandling();
$this->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [
new Child('a', 5400),
]))
->emailBillKind()
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
$this->withoutExceptionHandling()->login()->loginNami();
$invoice = Invoice::factory()
->to(ReceiverRequestFactory::new()->name('Familie Muster'))
->has(InvoicePosition::factory()->withMember(), 'positions')
->via(BillKind::EMAIL)
->create(['mail_email' => 'max@muster.de']);
InvoiceSendAction::run();
Mail::assertSent(PaymentMail::class, fn ($mail) => Storage::disk('temp')->path('rechnung-fur-mom.pdf') === $mail->filename && Storage::disk('temp')->exists('rechnung-fur-mom.pdf'));
Tex::assertCompiled(
BillDocument::class,
fn ($document) => 'Mom' === $document->familyName
&& $document->positions === ['tollerbeitrag 1997 für Lah Mom' => '54.00']
);
Tex::assertCompiledContent(BillDocument::class, BillDocument::from($member->payments->first()->invoice_data)->renderBody());
Mail::assertSent(BillMail::class, fn ($mail) => $mail->build() && $mail->hasTo('max@muster.de', 'Familie Muster') && Storage::disk('temp')->path('rechnung-fur-familie-muster.pdf') === $mail->filename && Storage::disk('temp')->exists('rechnung-fur-familie-muster.pdf'));
Tex::assertCompiled(BillDocument::class, fn ($document) => 'Familie Muster' === $document->toName);
$this->assertEquals(InvoiceStatus::SENT, $invoice->fresh()->status);
$this->assertEquals(now()->format('Y-m-d'), $invoice->fresh()->sent_at->format('Y-m-d'));
}
public function testItRemembersInvoices(): void
{
Mail::fake();
Tex::spy();
Storage::fake('temp');
$this->withoutExceptionHandling()->login()->loginNami();
$invoice = Invoice::factory()
->to(ReceiverRequestFactory::new()->name('Familie Muster'))
->has(InvoicePosition::factory()->withMember(), 'positions')
->via(BillKind::EMAIL)
->status(InvoiceStatus::SENT)
->create(['sent_at' => now()->subMonths(6), 'mail_email' => 'max@muster.de', 'last_remembered_at' => now()->subMonths(6)]);
InvoiceSendAction::run();
Mail::assertSent(RememberMail::class, fn ($mail) => $mail->build() && $mail->hasTo('max@muster.de', 'Familie Muster') && Storage::disk('temp')->path('zahlungserinnerung-fur-familie-muster.pdf') === $mail->filename && Storage::disk('temp')->exists('zahlungserinnerung-fur-familie-muster.pdf'));
Tex::assertCompiled(RememberDocument::class, fn ($document) => 'Familie Muster' === $document->toName);
$this->assertEquals(now()->format('Y-m-d'), $invoice->fresh()->last_remembered_at->format('Y-m-d'));
}
public function testItDoesntRememberWhenNotDue(): void
{
Mail::fake();
$this->withoutExceptionHandling()->login()->loginNami();
Invoice::factory()
->has(InvoicePosition::factory()->withMember(), 'positions')
->via(BillKind::EMAIL)
->status(InvoiceStatus::SENT)
->create(['sent_at' => now()->subMonths(6), 'last_remembered_at' => now()->subMonth()]);
InvoiceSendAction::run();
Mail::assertNotSent(RememberMail::class);
}
public function testItDoesntSendPostInvoices(): void
{
Mail::fake();
$this->withoutExceptionHandling()->login()->loginNami();
Invoice::factory()
->has(InvoicePosition::factory()->withMember(), 'positions')
->via(BillKind::POST)
->create();
InvoiceSendAction::run();
Mail::assertNotSent(BillMail::class);
}
}

View File

@ -30,7 +30,7 @@ class InvoiceStoreActionTest extends TestCase
'greeting' => 'Hallo Familie',
])
->position(InvoicePositionRequestFactory::new()->description('Beitrag Abc')->price(3250)->member($member))
->create()
->create(['mail_email' => 'a@b.de'])
);
$response->assertOk();
@ -38,6 +38,7 @@ class InvoiceStoreActionTest extends TestCase
'greeting' => 'Hallo Familie',
'via' => BillKind::POST->value,
'status' => InvoiceStatus::PAID->value,
'mail_email' => 'a@b.de',
]);
$invoice = Invoice::firstWhere('greeting', 'Hallo Familie');
$this->assertDatabaseHas('invoice_positions', [
@ -95,6 +96,11 @@ class InvoiceStoreActionTest extends TestCase
['via' => 'lala'],
['via' => 'Der gewählte Wert für Rechnungsweg ist ungültig.']
];
yield [
['usage' => ''],
['usage' => 'Verwendungszweck ist erforderlich.']
];
}
/**

View File

@ -68,7 +68,7 @@ class InvoiceUpdateActionTest extends TestCase
public function testItUpdatesAPosition(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$invoice = Invoice::factory()->has(InvoicePosition::factory(), 'positions')->create();
$invoice = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->create();
$this->patchJson(
route('invoice.update', ['invoice' => $invoice]),
@ -87,7 +87,7 @@ class InvoiceUpdateActionTest extends TestCase
public function testItDeletesAPosition(): void
{
$this->login()->loginNami()->withoutExceptionHandling();
$invoice = Invoice::factory()->has(InvoicePosition::factory(), 'positions')->create();
$invoice = Invoice::factory()->has(InvoicePosition::factory()->withMember(), 'positions')->create();
$this->patchJson(
route('invoice.update', ['invoice' => $invoice]),

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

@ -49,7 +49,7 @@ class MassStoreActionTest extends TestCase
->for(Subscription::factory()->children([
new Child('beitrag {name}', 4466),
new Child('beitrag2 für {name} für {year}', 2290),
]))->emailBillKind()->create(['firstname' => 'Max', 'lastname' => 'Muster', 'address' => 'Maxstr 4', 'zip' => '33445', 'location' => 'Solingen']);
]))->emailBillKind()->create(['firstname' => 'Max', 'lastname' => 'Muster', 'address' => 'Maxstr 4', 'zip' => '33445', 'location' => 'Solingen', 'email' => 'lala@b.de']);
$this->postJson(route('invoice.mass-store'), [
'year' => now()->addYear()->year,
@ -63,6 +63,8 @@ class MassStoreActionTest extends TestCase
'zip' => '33445',
'location' => 'Solingen',
], $invoice->to);
$this->assertEquals('Mitgliedsbeitrag für Muster', $invoice->usage);
$this->assertEquals('lala@b.de', $invoice->mail_email);
$this->assertEquals(BillKind::EMAIL, $invoice->via);
$this->assertDatabaseHas('invoice_positions', [
'invoice_id' => $invoice->id,

View File

@ -0,0 +1,38 @@
<?php
namespace Tests\Feature\Payment;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\MemberPaymentBlock;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class MemberPaymentBlockTest extends TestCase
{
use DatabaseTransactions;
public function testItHasData(): void
{
$this->login()->loginNami();
$member = Member::factory()->defaults()->create();
Member::factory()->defaults()->create();
Invoice::factory()
->has(InvoicePosition::factory()->price(3500)->for($member), 'positions')
->has(InvoicePosition::factory()->price(1000)->for($member), 'positions')
->status(InvoiceStatus::SENT)->create();
Invoice::factory()->has(InvoicePosition::factory()->price(600)->for($member), 'positions')->status(InvoiceStatus::NEW)->create();
Invoice::factory()->has(InvoicePosition::factory()->price(1000)->for($member), 'positions')->status(InvoiceStatus::PAID)->create();
$data = app(MemberPaymentBlock::class)->render()['data'];
$this->assertEquals([
'amount' => '51,00 €',
'members' => 1,
'total_members' => 2,
], $data);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Tests\Feature\Invoice;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\InvoiceSettings;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Zoomyboy\Tex\Tex;
class ShowPdfTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsAnInvoiceAsPdf(): void
{
Tex::spy();
InvoiceSettings::fake([
'from_long' => 'langer Stammesname',
'from' => 'Stammeskurz',
'mobile' => '+49 176 55555',
'email' => 'max@muster.de',
'website' => 'https://example.com',
'address' => 'Musterstr 4',
'place' => 'Münster',
'zip' => '12345',
'iban' => 'DE444',
'bic' => 'SOLSSSSS',
]);
$this->login()->loginNami();
$invoice = Invoice::factory()
->to(ReceiverRequestFactory::new()->name('Familie Lala'))
->has(InvoicePosition::factory()->withMember()->price(1500)->description('Beitrag12'), 'positions')
->via(BillKind::EMAIL)
->create(['usage' => 'Usa']);
$this->get(route('invoice.pdf', ['invoice' => $invoice]))
->assertOk()
->assertPdfPageCount(1)
->assertPdfName('rechnung-fur-familie-lala.pdf');
Tex::assertCompiled(BillDocument::class, fn ($document) => $document->hasAllContent([
'Beitrag12',
'Familie Lala',
'Rechnung',
'15.00',
'Usa',
'langer Stammesname',
'Stammeskurz',
'+49 176 55555',
'max@muster.de',
'https://example.com',
'Musterstr 4',
'Münster',
'12345',
'DE444',
'SOLSSSSS',
]));
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Tests\Feature\Invoice;
use App\Invoice\BillKind;
use App\Invoice\InvoiceSettings;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Invoice\RememberDocument;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Zoomyboy\Tex\Tex;
class ShowRememberpdfTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsRememberAsPdf(): void
{
Tex::spy();
InvoiceSettings::fake([
'from_long' => 'langer Stammesname',
'from' => 'Stammeskurz',
'mobile' => '+49 176 55555',
'email' => 'max@muster.de',
'website' => 'https://example.com',
'address' => 'Musterstr 4',
'place' => 'Münster',
'zip' => '12345',
'iban' => 'DE444',
'bic' => 'SOLSSSSS',
]);
$this->login()->loginNami();
$invoice = Invoice::factory()
->to(ReceiverRequestFactory::new()->name('Familie Lala'))
->has(InvoicePosition::factory()->withMember()->price(1500)->description('Beitrag12'), 'positions')
->via(BillKind::EMAIL)
->create(['usage' => 'Usa']);
$this->get(route('invoice.rememberpdf', ['invoice' => $invoice]))
->assertOk()
->assertPdfPageCount(1)
->assertPdfName('zahlungserinnerung-fur-familie-lala.pdf');
Tex::assertCompiled(RememberDocument::class, fn ($document) => $document->hasAllContent([
'Beitrag12',
'Familie Lala',
'Zahlungserinnerung',
'15.00',
'Usa',
'langer Stammesname',
'Stammeskurz',
'+49 176 55555',
'max@muster.de',
'https://example.com',
'Musterstr 4',
'Münster',
'12345',
'DE444',
'SOLSSSSS',
]));
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Tests\Feature\InvoicePosition;
use App\Invoice\Enums\InvoiceStatus;
use App\Invoice\Models\Invoice;
use App\Invoice\Models\InvoicePosition;
use App\Member\Member;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class IndexTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsInvoicePositions(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->has(InvoicePosition::factory()->for(Invoice::factory()->status(InvoiceStatus::SENT))->description('lala b')->price(5566))
->defaults()->create();
$this->get(route('member.invoice-position.index', ['member' => $member]))
->assertJsonPath('data.0.description', 'lala b')
->assertJsonPath('data.0.price_human', '55,66 €')
->assertJsonPath('data.0.id', $member->invoicePositions->first()->id)
->assertJsonPath('data.0.invoice.status', 'Rechnung gestellt');
}
}

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;
@ -37,7 +40,7 @@ class IndexTest extends TestCase
$this->assertInertiaHas($group->id, $response, 'data.data.0.group_id');
$this->assertInertiaHas(null, $response, 'data.data.0.memberships');
$this->assertInertiaHas(url("/member/{$member->id}/membership"), $response, 'data.data.0.links.membership_index');
$this->assertInertiaHas(url("/member/{$member->id}/payment"), $response, 'data.data.0.links.payment_index');
$this->assertInertiaHas(url("/member/{$member->id}/invoice-position"), $response, 'data.data.0.links.invoiceposition_index');
$this->assertInertiaHas(url("/member/{$member->id}/course"), $response, 'data.data.0.links.course_index');
$this->assertInertiaHas([
'id' => $member->subscription->id,
@ -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,65 +0,0 @@
<?php
namespace Tests\Feature\Payment;
use App\Invoice\BillDocument;
use App\Invoice\DocumentFactory;
use App\Member\Member;
use App\Payment\Payment;
use App\Payment\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Collection;
use Tests\RequestFactories\Child;
use Tests\TestCase;
class IndexTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsPayments(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->has(Payment::factory()->notPaid()->nr('2019')->subscription('Free', [
new Child('a', 1000),
new Child('b', 50),
]))
->defaults()->create();
$payment = $member->payments->first();
$this->get("/member/{$member->id}/payment")
->assertJsonPath('data.0.subscription.name', 'Free')
->assertJsonPath('data.0.subscription.id', $payment->subscription->id)
->assertJsonPath('data.0.subscription.amount', 1050)
->assertJsonPath('data.0.subscription_id', $payment->subscription->id)
->assertJsonPath('data.0.status_name', 'Nicht bezahlt')
->assertJsonPath('data.0.nr', '2019')
->assertJsonPath('data.0.links.show', null)
->assertJsonPath('data.0.links.update', url("/payment/{$payment->id}"))
->assertJsonPath('data.0.links.destroy', url("/payment/{$payment->id}"))
->assertJsonPath('meta.statuses.0.name', 'Nicht bezahlt')
->assertJsonPath('meta.statuses.0.id', $payment->status->id)
->assertJsonPath('meta.subscriptions.0.id', Subscription::first()->id)
->assertJsonPath('meta.subscriptions.0.name', Subscription::first()->name)
->assertJsonPath('meta.links.store', url("/member/{$member->id}/payment"));
}
public function testItShowsPaymentLink(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->has(Payment::factory()->notPaid()->nr('2019')->subscription('Free', [
new Child('a', 1000),
new Child('b', 50),
]))
->defaults()->create();
/** @var Collection<int|string, Member> */
$members = collect([$member]);
app(DocumentFactory::class)->afterSingle(BillDocument::fromMembers($members), $members);
$this->get("/member/{$member->id}/payment")
->assertJsonPath('data.0.links.show', route('payment.pdf', ['payment' => $member->payments->first()]));
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace Tests\Feature\Payment;
use App\Member\Member;
use App\Payment\MemberPaymentBlock;
use App\Payment\Payment;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child;
use Tests\TestCase;
class MemberPaymentsBlockTest extends TestCase
{
use DatabaseTransactions;
public function testItHasData(): void
{
$this->login()->loginNami();
Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->subscription('example', [
new Child('gg', 3400),
new Child('gg', 100),
]))
->create();
Member::factory()
->defaults()
->create();
$data = app(MemberPaymentBlock::class)->render()['data'];
$this->assertEquals([
'amount' => '35,00 €',
'members' => 1,
'total_members' => 2,
], $data);
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace Tests\Feature\Payment;
use App\Invoice\BillDocument;
use App\Invoice\DocumentFactory;
use App\Member\Member;
use App\Payment\Payment;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Collection;
use Tests\RequestFactories\Child;
use Tests\TestCase;
class PaymentPdfTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsAnInvoiceAsPdf(): void
{
$this->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [
new Child('a', 5400),
]))
->emailBillKind()
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
/** @var Collection<(int|string), Member> */
$members = collect([$member]);
app(DocumentFactory::class)->afterSingle(BillDocument::fromMembers($members), $members);
$response = $this->get(route('payment.pdf', ['payment' => $member->payments->first()]));
$response->assertOk();
$this->assertPdfPageCount(1, $response->getFile());
}
public function testItReturnsNoPdfWhenPaymentDoesntHaveInvoiceData(): void
{
$this->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [
new Child('a', 5400),
]))
->emailBillKind()
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
$response = $this->get(route('payment.pdf', ['payment' => $member->payments->first()]));
$response->assertStatus(204);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace Tests\Feature\Payment;
use App\Lib\Events\JobFinished;
use App\Lib\Events\JobStarted;
use App\Lib\Events\ReloadTriggered;
use App\Member\Member;
use App\Payment\Status;
use App\Payment\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;
class StoreTest extends TestCase
{
use DatabaseTransactions;
public function testItStoresAPayment(): void
{
Event::fake([JobStarted::class, JobFinished::class, ReloadTriggered::class]);
$this->withoutExceptionHandling()->login()->loginNami();
$subscription = Subscription::factory()->create();
$member = Member::factory()->defaults()->create();
$status = Status::factory()->create();
$this->post("/member/{$member->id}/payment", [
'status_id' => $status->id,
'subscription_id' => $subscription->id,
'nr' => '2019',
])->assertOk();
$this->assertDatabaseHas('payments', [
'member_id' => $member->id,
'status_id' => $status->id,
'subscription_id' => $subscription->id,
'nr' => '2019',
]);
Event::assertDispatched(JobStarted::class, fn ($event) => $event->broadcastOn()[0]->name === 'jobs' && $event->message !== null);
Event::assertDispatched(JobFinished::class, fn ($event) => $event->broadcastOn()[0]->name === 'jobs' && $event->message !== null);
Event::assertDispatched(ReloadTriggered::class, fn ($event) => ['member', 'payment'] === $event->channels->toArray());
}
}

View File

@ -1,131 +0,0 @@
<?php
namespace Tests\Feature\Sendpayment;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\InvoiceSettings;
use App\Invoice\Queries\BillKindQuery;
use App\Invoice\Queries\SingleMemberQuery;
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);
}
}

View File

@ -111,16 +111,6 @@ abstract class TestCase extends BaseTestCase
return $this;
}
public function assertPdfPageCount(int $pageCount, File $file): void
{
$this->assertTrue(file_exists($file->getPathname()));
exec('pdfinfo ' . escapeshellarg($file->getPathname()) . ' | grep ^Pages | sed "s/Pages:\s*//"', $output, $returnVar);
$this->assertSame(0, $returnVar, 'Failed to get Pages of PDF File ' . $file->getPathname());
$this->assertCount(1, $output, 'Failed to parse output format of pdfinfo');
$this->assertEquals($pageCount, $output[0]);
}
public function initInertiaTestcase(): void
{
TestResponse::macro('assertInertiaPath', function ($path, $value) {
@ -132,5 +122,27 @@ abstract class TestCase extends BaseTestCase
$json->assertPath($path, $value);
return $this;
});
TestResponse::macro('assertPdfPageCount', function (int $count) {
/** @var TestResponse */
$response = $this;
$file = $response->getFile();
Assert::assertTrue(file_exists($file->getPathname()));
exec('pdfinfo ' . escapeshellarg($file->getPathname()) . ' | grep ^Pages | sed "s/Pages:\s*//"', $output, $returnVar);
Assert::assertSame(0, $returnVar, 'Failed to get Pages of PDF File ' . $file->getPathname());
Assert::assertCount(1, $output, 'Failed to parse output format of pdfinfo');
Assert::assertEquals($count, $output[0]);
return $this;
});
TestResponse::macro('assertPdfName', function (string $filename) {
/** @var TestResponse */
$response = $this;
Assert::assertEquals($filename, $response->getFile()->getFilename());
return $this;
});
}
}

View File

@ -6,6 +6,8 @@ use Symfony\Component\HttpFoundation\File\File;
/**
* @method self assertInertiaPath(string $path, string|array<string, mixed>|int $value)
* @method self assertPdfPageCount(int $count)
* @method self assertPdfName(string $filename)
* @method File getFile()
*/
class TestResponse