Compare commits
17 Commits
0f80844d20
...
5d5b08ed78
Author | SHA1 | Date |
---|---|---|
|
5d5b08ed78 | |
|
3697885bca | |
|
2a6fd1152b | |
|
156b92f765 | |
|
afbfdf7ca2 | |
|
be29a284d5 | |
|
68a654494d | |
|
f5ad0d46fd | |
|
8d0d05463f | |
|
19dea7a061 | |
|
f4dc8b24bc | |
|
b0534279b6 | |
|
551c658fa3 | |
|
07a0c22a69 | |
|
2e8c41d5d9 | |
|
703c74a9f4 | |
|
bf8f6c87a8 |
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, '.', '');
|
||||
}
|
||||
}
|
|
@ -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, '.', '');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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, ',', '.') . ' €',
|
||||
];
|
||||
}
|
||||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ',', '') . ' €',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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]),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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
|
20
phpstan.neon
20
phpstan.neon
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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: {},
|
||||
|
|
|
@ -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,15 +105,18 @@
|
|||
</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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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::']);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
]));
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
]));
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()]));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue