Mitgliedschaft: Erinnerungen und eigenes Verlaengerungs-Produkt
Die Erinnerungsmails vor Ablauf der Mitgliedschaft beschrieben noch den abgeschafften SEPA-Einzug und verlinkten auf die Hauptdomain, wo /user/membership nicht existiert. Das Produkt fuer die Verlaengerung war zudem dasselbe wie bei der Registrierung und sprach deshalb vom Neuabschluss. - Erinnerungen: nur noch Stufen 31/33/34/35, SEPA-Stufen 32/36/37 samt :price und :pay_date entfallen; Button verlinkt auf my.mivita.care; fehlt ein Text zum Status, wird die Mail geloggt statt roh versendet. - Alle Erinnerungstexte (de/en/es/fr) erklaeren jetzt, dass ohne Zahlung der Account pausiert und offene Provisionen ausgezahlt werden. - Eigenes Produkt fuer die Verlaengerung (identifier show_renewal) als Kopie des Mitgliedschafts-Produkts inkl. Preisen, Kategorien und Uebersetzungen fuer en/es/fr; die Mitgliedschaftsseite faellt ohne dieses Produkt auf das bisherige zurueck. - Mitgliedschaftsseite auf ein Paket reduziert: Upgrade-, Wechsel- und Abo-Optionen entfernt, Texte auf die manuelle Verlaengerung umgestellt. - Alte Cron-Weboberflaeche entfernt, der Versand laeuft ueber den Scheduler (payments:check-accounts). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1894d2db67
commit
c3dc7eb35e
30 changed files with 810 additions and 1223 deletions
83
tests/Feature/MembershipReminderLinkTest.php
Normal file
83
tests/Feature/MembershipReminderLinkTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
use App\Mail\MailCustomMessage;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Der Verlängerungs-Link aus der Mitgliedschafts-Erinnerung muss auf das CRM
|
||||
* (my.mivita.care) zeigen. Die Route /user/membership existiert nur dort –
|
||||
* auf der Hauptdomain lief der Link in einen 404.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
// Produktivähnliche Domains, sonst greift Util::isTestSystem() und es wird nichts versendet.
|
||||
config([
|
||||
'app.tld_care' => '.care',
|
||||
'app.tld_shop' => '.shop',
|
||||
'app.domain' => 'mivita',
|
||||
'app.pre_url_crm' => 'my.',
|
||||
'app.protocol' => 'https://',
|
||||
'app.url' => 'https://mivita.care/',
|
||||
]);
|
||||
|
||||
Mail::fake();
|
||||
});
|
||||
|
||||
function makeReminderUser(int $daysLeft): User
|
||||
{
|
||||
if (! User::find(1)) {
|
||||
User::forceCreate([
|
||||
'id' => 1,
|
||||
'email' => 'system@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
}
|
||||
|
||||
return User::forceCreate([
|
||||
'email' => 'berater-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
'blocked' => 0,
|
||||
'payment_account' => now()->addDays($daysLeft),
|
||||
]);
|
||||
}
|
||||
|
||||
it('verlinkt die Mitgliedschafts-Verlängerung auf die CRM-Domain', function () {
|
||||
makeReminderUser(10);
|
||||
|
||||
$this->artisan('payments:check-accounts')->assertSuccessful();
|
||||
|
||||
Mail::assertSent(MailCustomMessage::class, function (MailCustomMessage $mail) {
|
||||
return str_contains($mail->render(), 'https://my.mivita.care/user/membership');
|
||||
});
|
||||
});
|
||||
|
||||
it('verlinkt nicht mehr auf die Hauptdomain', function () {
|
||||
makeReminderUser(10);
|
||||
|
||||
$this->artisan('payments:check-accounts')->assertSuccessful();
|
||||
|
||||
Mail::assertSent(MailCustomMessage::class, function (MailCustomMessage $mail) {
|
||||
return ! str_contains($mail->render(), 'https://mivita.care//user/membership')
|
||||
&& ! str_contains($mail->render(), 'https://mivita.care/user/membership');
|
||||
});
|
||||
});
|
||||
|
||||
it('erklaert in der Erinnerung, was beim Ablauf der Mitgliedschaft passiert', function () {
|
||||
makeReminderUser(10);
|
||||
|
||||
$this->artisan('payments:check-accounts')->assertSuccessful();
|
||||
|
||||
Mail::assertSent(MailCustomMessage::class, function (MailCustomMessage $mail) {
|
||||
$body = $mail->render();
|
||||
|
||||
return str_contains($body, 'keine Punkte und keine Provisionen mehr gutgeschrieben')
|
||||
&& str_contains($body, 'im nächsten Zahlungslauf aus');
|
||||
});
|
||||
});
|
||||
186
tests/Feature/MembershipRenewalProductTest.php
Normal file
186
tests/Feature/MembershipRenewalProductTest.php
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\User\MembershipController;
|
||||
use App\Models\Product;
|
||||
use App\Models\TransProduct;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Die Verlängerung braucht ein eigenes Produkt: das Registrierungs-Produkt
|
||||
* (show_order) beschreibt den Neuabschluss und passt inhaltlich nicht.
|
||||
*/
|
||||
function makeMembershipProduct(string $identifier, array $showOn, string $name, int $pos = 20): Product
|
||||
{
|
||||
$now = now();
|
||||
$id = (int) DB::table('products')->insertGetId([
|
||||
'name' => $name,
|
||||
'title' => '12 Monate Laufzeit',
|
||||
'copy' => '<p>Du kannst Dich jetzt mit der Jahresgebühr registrieren.</p>',
|
||||
'identifier' => $identifier,
|
||||
'active' => true,
|
||||
'price' => 69.90,
|
||||
'tax' => 19,
|
||||
'weight' => 0,
|
||||
'points' => 0,
|
||||
'pos' => $pos,
|
||||
'is_membership_only' => 1,
|
||||
'action' => json_encode(['0', '1']),
|
||||
'show_on' => json_encode($showOn),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return Product::findOrFail($id);
|
||||
}
|
||||
|
||||
function runRenewalProductMigration(): void
|
||||
{
|
||||
(require base_path('database/migrations/2026_08_25_120000_create_membership_renewal_product.php'))->up();
|
||||
}
|
||||
|
||||
function renewalProductsOfController(): Collection
|
||||
{
|
||||
$method = new ReflectionMethod(MembershipController::class, 'renewalProducts');
|
||||
$method->setAccessible(true);
|
||||
|
||||
return collect($method->invoke(new MembershipController));
|
||||
}
|
||||
|
||||
it('legt das Verlängerungs-Produkt als Kopie des Registrierungs-Produkts an', function () {
|
||||
$registration = makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
|
||||
runRenewalProductMigration();
|
||||
|
||||
$renewal = Product::where('identifier', 'show_renewal')->firstOrFail();
|
||||
|
||||
expect($renewal->id)->not->toBe($registration->id)
|
||||
->and($renewal->name)->toBe($registration->name)
|
||||
->and((float) $renewal->price)->toBe((float) $registration->price)
|
||||
->and($renewal->action)->toBe($registration->action)
|
||||
->and((bool) $renewal->is_membership_only)->toBeTrue()
|
||||
->and($renewal->show_on)->toBe(['8'])
|
||||
->and($renewal->slug)->not->toBe($registration->slug);
|
||||
});
|
||||
|
||||
it('spricht im Verlängerungs-Text von der Verlängerung statt von der Registrierung', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
|
||||
runRenewalProductMigration();
|
||||
|
||||
$renewal = Product::where('identifier', 'show_renewal')->firstOrFail();
|
||||
|
||||
expect($renewal->copy)->toContain('verlängerst Deine MIVITA Mitgliedschaft')
|
||||
->and($renewal->copy)->not->toContain('registrieren');
|
||||
});
|
||||
|
||||
it('laesst das Registrierungs-Produkt unveraendert', function () {
|
||||
$registration = makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
|
||||
runRenewalProductMigration();
|
||||
|
||||
expect($registration->fresh()->show_on)->toBe(['7', '8'])
|
||||
->and($registration->fresh()->identifier)->toBe('show_order');
|
||||
});
|
||||
|
||||
it('legt kein zweites Verlängerungs-Produkt an', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
|
||||
runRenewalProductMigration();
|
||||
runRenewalProductMigration();
|
||||
|
||||
expect(Product::where('identifier', 'show_renewal')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('zeigt bei der Verlängerung nur noch das Verlängerungs-Produkt', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
runRenewalProductMigration();
|
||||
|
||||
$products = renewalProductsOfController();
|
||||
|
||||
expect($products)->toHaveCount(1)
|
||||
->and($products->first()->identifier)->toBe('show_renewal');
|
||||
});
|
||||
|
||||
it('faellt ohne Verlängerungs-Produkt auf das Mitgliedschafts-Produkt zurueck', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
|
||||
$products = renewalProductsOfController();
|
||||
|
||||
expect($products)->toHaveCount(1)
|
||||
->and($products->first()->identifier)->toBe('show_order');
|
||||
});
|
||||
|
||||
it('ignoriert ein inaktives Verlängerungs-Produkt', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
runRenewalProductMigration();
|
||||
Product::where('identifier', 'show_renewal')->update(['active' => false]);
|
||||
|
||||
$products = renewalProductsOfController();
|
||||
|
||||
expect($products)->toHaveCount(1)
|
||||
->and($products->first()->identifier)->toBe('show_order');
|
||||
});
|
||||
|
||||
function runRenewalTranslationMigration(): void
|
||||
{
|
||||
(require base_path('database/migrations/2026_08_25_130000_translate_membership_renewal_product.php'))->up();
|
||||
}
|
||||
|
||||
function renewalCopyIn(string $locale): string
|
||||
{
|
||||
app()->setLocale($locale);
|
||||
|
||||
return Product::where('identifier', 'show_renewal')->firstOrFail()->getLang('copy');
|
||||
}
|
||||
|
||||
it('uebersetzt die Verlängerungs-Beschreibung in alle Sprachen', function (string $locale, string $needle) {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
runRenewalProductMigration();
|
||||
runRenewalTranslationMigration();
|
||||
|
||||
expect(renewalCopyIn($locale))->toContain($needle)
|
||||
->and(renewalCopyIn($locale))->not->toContain('verlängerst Deine MIVITA Mitgliedschaft');
|
||||
})->with([
|
||||
'en' => ['en', 'renewing your MIVITA membership'],
|
||||
'es' => ['es', 'Amplías tu afiliación MIVITA'],
|
||||
'fr' => ['fr', 'Vous renouvelez votre adhésion MIVITA'],
|
||||
]);
|
||||
|
||||
it('behaelt den deutschen Text als Standard', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
runRenewalProductMigration();
|
||||
runRenewalTranslationMigration();
|
||||
|
||||
expect(renewalCopyIn('de'))->toContain('verlängerst Deine MIVITA Mitgliedschaft');
|
||||
});
|
||||
|
||||
it('uebernimmt den uebersetzten Produktnamen aus dem Mitgliedschafts-Produkt', function () {
|
||||
$registration = makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
TransProduct::create(['language' => 'en', 'product_id' => $registration->id, 'key' => 'name', 'value' => 'MIVITA BUSINESS package']);
|
||||
|
||||
runRenewalProductMigration();
|
||||
runRenewalTranslationMigration();
|
||||
|
||||
app()->setLocale('en');
|
||||
|
||||
expect(Product::where('identifier', 'show_renewal')->firstOrFail()->getLang('name'))->toBe('MIVITA BUSINESS package');
|
||||
});
|
||||
|
||||
it('ueberschreibt nachtraeglich angepasste Uebersetzungen nicht', function () {
|
||||
makeMembershipProduct('show_order', ['7', '8'], 'MIVITA BUSINESS Paket');
|
||||
runRenewalProductMigration();
|
||||
|
||||
$renewal = Product::where('identifier', 'show_renewal')->firstOrFail();
|
||||
TransProduct::create(['language' => 'en', 'product_id' => $renewal->id, 'key' => 'copy', 'value' => '<p>Handmade</p>']);
|
||||
|
||||
runRenewalTranslationMigration();
|
||||
runRenewalTranslationMigration();
|
||||
|
||||
expect(renewalCopyIn('en'))->toBe('<p>Handmade</p>')
|
||||
->and(TransProduct::where('product_id', $renewal->id)->where('key', 'copy')->count())->toBe(3);
|
||||
});
|
||||
58
tests/Feature/MembershipViewTextsTest.php
Normal file
58
tests/Feature/MembershipViewTextsTest.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
/**
|
||||
* Absicherung der Mitgliedschafts-Ansichten nach der Zusammenlegung auf ein Paket:
|
||||
* die Templates müssen kompilieren und jeder verwendete Sprachschlüssel muss in
|
||||
* allen vier Sprachen existieren.
|
||||
*/
|
||||
$membershipViews = [
|
||||
'resources/views/user/membership/index.blade.php',
|
||||
'resources/views/user/membership/_payment_order.blade.php',
|
||||
'resources/views/dashboard/_reminder.blade.php',
|
||||
'resources/views/dashboard/_membership.blade.php',
|
||||
];
|
||||
|
||||
it('kompiliert die Mitgliedschafts-Templates zu gültigem PHP', function (string $view) {
|
||||
$compiled = Blade::compileString(file_get_contents(base_path($view)));
|
||||
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'blade').'.php';
|
||||
file_put_contents($tmp, $compiled);
|
||||
exec('php -l '.escapeshellarg($tmp).' 2>&1', $output, $exitCode);
|
||||
unlink($tmp);
|
||||
|
||||
expect($exitCode)->toBe(0, implode("\n", $output));
|
||||
})->with($membershipViews);
|
||||
|
||||
it('kennt jeden verwendeten Sprachschlüssel in allen Sprachen', function (string $view) {
|
||||
preg_match_all(
|
||||
"/__\('(membership|reminder)\.([a-zA-Z0-9_]+)'/",
|
||||
file_get_contents(base_path($view)),
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
);
|
||||
|
||||
expect($matches)->not->toBeEmpty();
|
||||
|
||||
foreach (['de', 'en', 'es', 'fr'] as $locale) {
|
||||
foreach ($matches as [$_, $file, $key]) {
|
||||
$translations = include base_path("resources/lang/$locale/$file.php");
|
||||
expect(array_key_exists($key, $translations))
|
||||
->toBeTrue("$locale/$file.php fehlt '$key' (aus $view)");
|
||||
}
|
||||
}
|
||||
})->with(array_diff($membershipViews, ['resources/views/dashboard/_membership.blade.php']));
|
||||
|
||||
it('beschreibt nirgends mehr die abgeschaffte automatische Vertragsverlängerung', function (string $locale) {
|
||||
$membership = include base_path("resources/lang/$locale/membership.php");
|
||||
|
||||
foreach (['info_contract_renewal', 'membership_was_renewed', 'contract_renewal', 'change_copy_1', 'change_copy_2'] as $obsolete) {
|
||||
expect($membership)->not->toHaveKey($obsolete);
|
||||
}
|
||||
|
||||
expect($membership)->toHaveKeys(['renewal_hl', 'renewal_info', 'renewal_expired_info', 'renewal_consequence', 'active_info', 'renewal_possible_from']);
|
||||
})->with(['de', 'en', 'es', 'fr']);
|
||||
Loading…
Add table
Add a link
Reference in a new issue