Incentive: Schalter "Abos beruecksichtigen", Recruiting Challenge ohne Abos
Die Recruiting Challenge erfasste Eigen- und Kundenabos mit 0 Punkten (Live-Kopie: 175 Eigenabos). Sie tauchten in Details und Admin auf und brachten Berater ohne Neupartner in die Rangliste. - incentives.include_abos (Standard an); Migration schaltet Incentives ohne jede Abo-Wertung aus (0 Abo-Punkte, 0 Mindestabos, ohne Umsatzpunkte) - aus: kein Abo-Tracking in Tracker, Neuaufbau und Reparatur; incentive:calculate raeumt vorhandene Abo-Zeilen ab - Admin: Checkbox im Formular, Abo-Punkte/Mindestabos werden dann auf 0 gesetzt; Abo-Spalte und -Bereiche in Admin, Details und Teilnahme-Hinweis entfallen - Seeder der Recruiting Challenge setzt den Schalter aus Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
773d523bf7
commit
11afdb11d1
19 changed files with 306 additions and 34 deletions
|
|
@ -97,6 +97,7 @@ class IncentiveCalculate extends Command
|
|||
'tracking_partner_added' => 0,
|
||||
'tracking_partner_removed' => 0,
|
||||
'tracking_abo_added' => 0,
|
||||
'tracking_abo_removed' => 0,
|
||||
'repair_partner_fk' => 0,
|
||||
'repair_abo_fk' => 0,
|
||||
'repair_onetime_partner_fk' => 0,
|
||||
|
|
@ -130,6 +131,7 @@ class IncentiveCalculate extends Command
|
|||
$stats['repair_onetime_abo_fk'] += $r['onetime_abo_fk'];
|
||||
$stats['tracking_partner_removed'] += $repairService->removeStaleSponsorTrackings($participant);
|
||||
$stats['tracking_partner_added'] += $repairService->syncMissingTrackingPartners($participant);
|
||||
$stats['tracking_abo_removed'] += $repairService->removeExcludedAboTrackings($participant);
|
||||
$stats['tracking_abo_added'] += $repairService->syncMissingTrackingAbos($participant);
|
||||
$stats['sv_logs_added'] += $repairService->syncMissingSalesVolumeLogs($participant);
|
||||
}
|
||||
|
|
@ -186,6 +188,7 @@ class IncentiveCalculate extends Command
|
|||
$tableRows[] = ['Neupartner-Trackings nachgezogen', (string) $stats['tracking_partner_added']];
|
||||
$tableRows[] = ['Neupartner-Trackings entfernt (Sponsorwechsel)', (string) $stats['tracking_partner_removed']];
|
||||
$tableRows[] = ['Neuabo-Trackings nachgezogen', (string) $stats['tracking_abo_added']];
|
||||
$tableRows[] = ['Abo-Trackings entfernt (Incentive ohne Abos)', (string) $stats['tracking_abo_removed']];
|
||||
$tableRows[] = ['FK Partner (akkum.) repariert', (string) $stats['repair_partner_fk']];
|
||||
$tableRows[] = ['FK Abo (akkum.) repariert', (string) $stats['repair_abo_fk']];
|
||||
$tableRows[] = ['FK Partner (Einmal) repariert', (string) $stats['repair_onetime_partner_fk']];
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class IncentiveController extends Controller
|
|||
'bonus_rank_2' => 'nullable|numeric|min:0|max:999999',
|
||||
'bonus_rank_3' => 'nullable|numeric|min:0|max:999999',
|
||||
'include_sales_points' => 'nullable|boolean',
|
||||
'include_abos' => 'nullable|boolean',
|
||||
'vip_only' => 'nullable|boolean',
|
||||
'status' => 'required|integer|in:0,1,2',
|
||||
]);
|
||||
|
|
@ -112,7 +113,9 @@ class IncentiveController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Leere Betragsfelder als NULL speichern, Checkbox als bool.
|
||||
* Leere Betragsfelder als NULL speichern, Checkboxen als bool. Ohne Abos werden
|
||||
* Abo-Einmalpunkte und Mindestabos auf 0 gesetzt, sonst waere die Qualifikation
|
||||
* unerreichbar.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
|
|
@ -124,6 +127,11 @@ class IncentiveController extends Controller
|
|||
}
|
||||
$data['include_sales_points'] = filter_var($data['include_sales_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$data['vip_only'] = filter_var($data['vip_only'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
$data['include_abos'] = filter_var($data['include_abos'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
if (! $data['include_abos']) {
|
||||
$data['points_abo_onetime'] = 0;
|
||||
$data['min_customer_abos'] = 0;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
|
@ -159,6 +167,7 @@ class IncentiveController extends Controller
|
|||
'bonus_rank_2' => 'nullable|numeric|min:0|max:999999',
|
||||
'bonus_rank_3' => 'nullable|numeric|min:0|max:999999',
|
||||
'include_sales_points' => 'nullable|boolean',
|
||||
'include_abos' => 'nullable|boolean',
|
||||
'vip_only' => 'nullable|boolean',
|
||||
'status' => 'required|integer|in:0,1,2',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class IncentiveController extends Controller
|
|||
->paginate(100);
|
||||
|
||||
$participateHasTrackableAbos = false;
|
||||
if (! $participant?->accepted_terms_at) {
|
||||
if (! $participant?->accepted_terms_at && $incentive->includesAbos()) {
|
||||
$participateHasTrackableAbos = $this->userHasTrackableAbosForIncentive($user, $incentive);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class Incentive extends Model
|
|||
'bonus_rank_2' => 'float',
|
||||
'bonus_rank_3' => 'float',
|
||||
'include_sales_points' => 'bool',
|
||||
'include_abos' => 'bool',
|
||||
'status' => 'int',
|
||||
'vip_only' => 'bool',
|
||||
'qualification_start' => 'date',
|
||||
|
|
@ -101,6 +102,7 @@ class Incentive extends Model
|
|||
'bonus_rank_2',
|
||||
'bonus_rank_3',
|
||||
'include_sales_points',
|
||||
'include_abos',
|
||||
'status',
|
||||
'vip_only',
|
||||
];
|
||||
|
|
@ -365,6 +367,15 @@ class Incentive extends Model
|
|||
return $this->include_sales_points === null || (bool) $this->include_sales_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Werden Abos ueberhaupt erfasst? Aus = reine Recruiting-Challenge, weder Kunden-
|
||||
* noch Eigenabos landen im Tracking.
|
||||
*/
|
||||
public function includesAbos(): bool
|
||||
{
|
||||
return $this->include_abos === null || (bool) $this->include_abos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt es eine Mindestqualifikation (direkte Partner oder Kundenabos)?
|
||||
*/
|
||||
|
|
@ -378,7 +389,7 @@ class Incentive extends Model
|
|||
*/
|
||||
public function usesAbos(): bool
|
||||
{
|
||||
return $this->points_abo_onetime > 0 || $this->min_customer_abos > 0;
|
||||
return $this->includesAbos() && ($this->points_abo_onetime > 0 || $this->min_customer_abos > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -325,7 +325,12 @@ class IncentiveParticipant extends Model
|
|||
->where('created_at', '<', $qualStart)
|
||||
->get();
|
||||
|
||||
foreach ($customerAbosInPeriod->concat($ownAbosInPeriod)->concat($ownAbosPreExisting) as $abo) {
|
||||
// Incentive ohne Abos (reine Recruiting-Challenge): nichts erfassen
|
||||
$abosToTrack = $incentive->includesAbos()
|
||||
? $customerAbosInPeriod->concat($ownAbosInPeriod)->concat($ownAbosPreExisting)
|
||||
: collect();
|
||||
|
||||
foreach ($abosToTrack as $abo) {
|
||||
$activatedAt = $abo->created_at;
|
||||
$logMonth = (int) $abo->created_at->month;
|
||||
$logYear = (int) $abo->created_at->year;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,23 @@ class IncentivePointsLogRepairService
|
|||
return $removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incentive ohne Abos: bereits erfasste Abo-Trackings samt Log-Zeilen entfernen
|
||||
* (z. B. nachdem der Schalter im Admin ausgeschaltet wurde).
|
||||
*
|
||||
* @return int Anzahl entfernter Abo-Trackings
|
||||
*/
|
||||
public function removeExcludedAboTrackings(IncentiveParticipant $participant): int
|
||||
{
|
||||
if (! $participant->incentive || $participant->incentive->includesAbos()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$participant->pointsLog()->where('type', 'abo')->delete();
|
||||
|
||||
return $participant->newAbos()->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fehlende Abo-Tracking-Zeilen anlegen (Kundenabo ot / Eigenabo me, wie IncentiveTracker).
|
||||
* Zuerst trackAboActivated ueber Erstbestellung; ohne UserAboOrder manuell wie Neuaufbau B.
|
||||
|
|
@ -180,7 +197,7 @@ class IncentivePointsLogRepairService
|
|||
public function syncMissingTrackingAbos(IncentiveParticipant $participant): int
|
||||
{
|
||||
$incentive = $participant->incentive;
|
||||
if (! $incentive) {
|
||||
if (! $incentive || ! $incentive->includesAbos()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,8 @@ class IncentiveTracker
|
|||
$incentives = Incentive::query()
|
||||
->active()
|
||||
->qualificationWindowContains($activation_date)
|
||||
->get();
|
||||
->get()
|
||||
->filter(fn (Incentive $incentive) => $incentive->includesAbos());
|
||||
|
||||
foreach ($incentives as $incentive) {
|
||||
$participant = IncentiveParticipant::ensureForIncentiveUser($incentive, $consultant_id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Schalter "Abos beruecksichtigen": Aus = das Incentive erfasst weder Kunden- noch
|
||||
* Eigenabos (keine Tracking-Zeilen, keine Abo-Sektion in Teaser/Details). Fuer eine
|
||||
* reine Recruiting-Challenge, bei der nur Neupartner zaehlen.
|
||||
*
|
||||
* Bestehende Incentives ohne jede Abo-Wertung (0 Einmalpunkte, 0 Mindestabos, ohne
|
||||
* Umsatzpunkte) werden direkt umgestellt; ihre bereits erfassten Abo-Zeilen raeumt der
|
||||
* naechste Lauf von incentive:calculate ab.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('incentives', 'include_abos')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('incentives', function (Blueprint $table) {
|
||||
$table->boolean('include_abos')->default(true)->after('include_sales_points')
|
||||
->comment('0 = Abos werden nicht erfasst (reine Recruiting-Challenge)');
|
||||
});
|
||||
|
||||
DB::table('incentives')
|
||||
->where('points_abo_onetime', 0)
|
||||
->where('min_customer_abos', 0)
|
||||
->where('include_sales_points', false)
|
||||
->update(['include_abos' => false]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('incentives', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('incentives', 'include_abos')) {
|
||||
$table->dropColumn('include_abos');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -45,6 +45,7 @@ class RecruitingChallenge2026Seeder extends Seeder
|
|||
'bonus_rank_2' => 300,
|
||||
'bonus_rank_3' => 200,
|
||||
'include_sales_points' => false,
|
||||
'include_abos' => false,
|
||||
'status' => 0,
|
||||
'vip_only' => true,
|
||||
'image' => 'incentive-recruiting-2026.jpg',
|
||||
|
|
|
|||
|
|
@ -190,6 +190,8 @@ return [
|
|||
'bonus_rank' => 'Bonus Platz :n (€)',
|
||||
'include_sales_points' => 'Umsatzpunkte der Neupartner/Abos mitzählen',
|
||||
'include_sales_points_help' => 'Aus: Die Wertung besteht nur aus den Einmalpunkten (z. B. 1 Punkt je Neupartner = Rangliste nach Anzahl Neupartner).',
|
||||
'include_abos' => 'Abos berücksichtigen (Kunden- und Eigenabos)',
|
||||
'include_abos_help' => 'Aus: Das Incentive erfasst keine Abos. Abo-Punkte, Mindestanzahl Abos und die Abo-Bereiche in Teaser und Details entfallen (z. B. reine Recruiting-Challenge).',
|
||||
'payout_export' => 'Auszahlungs-Export (XLSX)',
|
||||
'payout_recipients' => 'Empfänger',
|
||||
'payout_partner_amount' => 'Prämie Neupartner',
|
||||
|
|
|
|||
|
|
@ -190,6 +190,8 @@ return [
|
|||
'bonus_rank' => 'Bonus rank :n (€)',
|
||||
'include_sales_points' => 'Count sales points of new partners/subscriptions',
|
||||
'include_sales_points_help' => 'Off: The score consists of one-time points only (e.g. 1 point per new partner = ranking by number of new partners).',
|
||||
'include_abos' => 'Include subscriptions (customer and own subscriptions)',
|
||||
'include_abos_help' => 'Off: The incentive does not track any subscriptions. Subscription points, minimum number of subscriptions and the subscription sections in teaser and details are omitted (e.g. pure recruiting challenge).',
|
||||
'payout_export' => 'Payout export (XLSX)',
|
||||
'payout_recipients' => 'Recipients',
|
||||
'payout_partner_amount' => 'New partner reward',
|
||||
|
|
|
|||
|
|
@ -190,6 +190,8 @@ return [
|
|||
'bonus_rank' => 'Bono puesto :n (€)',
|
||||
'include_sales_points' => 'Contar puntos de ventas de nuevos socios/suscripciones',
|
||||
'include_sales_points_help' => 'Desactivado: la puntuación consta solo de los puntos únicos (p. ej. 1 punto por nuevo socio = ranking por número de nuevos socios).',
|
||||
'include_abos' => 'Tener en cuenta suscripciones (de clientes y propias)',
|
||||
'include_abos_help' => 'Desactivado: el incentivo no registra suscripciones. Se omiten los puntos por suscripción, el número mínimo de suscripciones y las secciones de suscripciones en el teaser y los detalles (p. ej. challenge de reclutamiento pura).',
|
||||
'payout_export' => 'Exportación de pagos (XLSX)',
|
||||
'payout_recipients' => 'Destinatarios',
|
||||
'payout_partner_amount' => 'Prima nuevos socios',
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ return [
|
|||
'bonus_rank' => 'Bonus place :n (€)',
|
||||
'include_sales_points' => 'Compter les points de vente des nouveaux partenaires/abonnements',
|
||||
'include_sales_points_help' => 'Désactivé : le score ne comprend que les points uniques (p. ex. 1 point par nouveau partenaire = classement par nombre de nouveaux partenaires).',
|
||||
'include_abos' => 'Prendre en compte les abonnements (clients et personnels)',
|
||||
'include_abos_help' => 'Désactivé : l’incentive n’enregistre aucun abonnement. Les points d’abonnement, le nombre minimum d’abonnements et les sections abonnements du teaser et des détails sont supprimés (p. ex. challenge de recrutement pur).',
|
||||
'payout_export' => 'Export des versements (XLSX)',
|
||||
'payout_recipients' => 'Bénéficiaires',
|
||||
'payout_partner_amount' => 'Prime nouveaux partenaires',
|
||||
|
|
|
|||
|
|
@ -107,6 +107,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="hidden" name="include_abos" value="0">
|
||||
<input type="checkbox" class="custom-control-input" id="include_abos" name="include_abos" value="1"
|
||||
@if (old('include_abos', isset($incentive) ? (int) $incentive->includesAbos() : 1)) checked @endif>
|
||||
<label class="custom-control-label" for="include_abos">{{ __('incentive.include_abos') }}</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ __('incentive.include_abos_help') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="min_direct_partners">{{ __('incentive.min_direct_partners') }} *</label>
|
||||
|
|
|
|||
|
|
@ -26,16 +26,18 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center bg-light">
|
||||
<div class="card-body py-2">
|
||||
<small class="text-muted">{{ __('incentive.abos') }}</small>
|
||||
<h4 class="mb-0 font-weight-bold {{ $participant->qualified_abos >= $incentive->min_customer_abos ? 'text-success' : 'text-danger' }}">
|
||||
{{ $participant->qualified_abos }}/{{ $incentive->min_customer_abos }}
|
||||
</h4>
|
||||
@if ($incentive->includesAbos())
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center bg-light">
|
||||
<div class="card-body py-2">
|
||||
<small class="text-muted">{{ __('incentive.abos') }}</small>
|
||||
<h4 class="mb-0 font-weight-bold {{ $participant->qualified_abos >= $incentive->min_customer_abos ? 'text-success' : 'text-danger' }}">
|
||||
{{ $participant->qualified_abos }}/{{ $incentive->min_customer_abos }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Sektion A: Neupartner-Punkte --}}
|
||||
|
|
@ -50,12 +52,14 @@
|
|||
])
|
||||
</div>
|
||||
|
||||
{{-- Sektion B: Kundenabo-Punkte --}}
|
||||
<h6 class="font-weight-bold">{{ __('incentive.section_abos') }}</h6>
|
||||
@include('partials.incentive._source_table', [
|
||||
'sources' => $abo_sources,
|
||||
'type' => 'abo',
|
||||
'label_header' => __('incentive.customer_abo'),
|
||||
'date_header' => __('incentive.abo_date'),
|
||||
'empty_message' => __('incentive.no_abos_yet'),
|
||||
])
|
||||
{{-- Sektion B: Kundenabo-Punkte (entfaellt bei Incentives ohne Abos) --}}
|
||||
@if ($incentive->includesAbos())
|
||||
<h6 class="font-weight-bold">{{ __('incentive.section_abos') }}</h6>
|
||||
@include('partials.incentive._source_table', [
|
||||
'sources' => $abo_sources,
|
||||
'type' => 'abo',
|
||||
'label_header' => __('incentive.customer_abo'),
|
||||
'date_header' => __('incentive.abo_date'),
|
||||
'empty_message' => __('incentive.no_abos_yet'),
|
||||
])
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@
|
|||
<strong>{{ __('incentive.min_direct_partners') }}:</strong> {{ $incentive->min_direct_partners }}<br>
|
||||
<strong>{{ __('incentive.min_customer_abos') }}:</strong> {{ $incentive->min_customer_abos }}<br>
|
||||
<strong>{{ __('incentive.include_sales_points') }}:</strong>
|
||||
{{ $incentive->includesSalesPoints() ? __('incentive.yes') : __('incentive.no') }}
|
||||
{{ $incentive->includesSalesPoints() ? __('incentive.yes') : __('incentive.no') }}<br>
|
||||
<strong>{{ __('incentive.include_abos') }}:</strong>
|
||||
{{ $incentive->includesAbos() ? __('incentive.yes') : __('incentive.no') }}
|
||||
</div>
|
||||
</div>
|
||||
@if ($incentive->hasCashPayout())
|
||||
|
|
@ -103,7 +105,9 @@
|
|||
<th>{{ __('incentive.admin_terms_accepted') }}</th>
|
||||
<th>{{ __('incentive.total_points') }}</th>
|
||||
<th>{{ __('incentive.partners') }}</th>
|
||||
<th>{{ __('incentive.abos') }}</th>
|
||||
@if ($incentive->includesAbos())
|
||||
<th>{{ __('incentive.abos') }}</th>
|
||||
@endif
|
||||
<th>{{ __('incentive.qualified') }}</th>
|
||||
@if ($incentive->hasCashPayout())
|
||||
<th class="text-right">{{ __('incentive.payout_total') }}</th>
|
||||
|
|
@ -144,12 +148,14 @@
|
|||
<span class="text-success">✓</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
{{ $p->qualified_abos }}/{{ $incentive->min_customer_abos }}
|
||||
@if ($p->qualified_abos >= $incentive->min_customer_abos)
|
||||
<span class="text-success">✓</span>
|
||||
@endif
|
||||
</td>
|
||||
@if ($incentive->includesAbos())
|
||||
<td>
|
||||
{{ $p->qualified_abos }}/{{ $incentive->min_customer_abos }}
|
||||
@if ($p->qualified_abos >= $incentive->min_customer_abos)
|
||||
<span class="text-success">✓</span>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
<td>
|
||||
@if ($p->is_qualified)
|
||||
<span class="badge badge-success">{{ __('incentive.yes') }}</span>
|
||||
|
|
@ -189,7 +195,7 @@
|
|||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $incentive->hasCashPayout() ? 11 : 10 }}" class="text-center text-muted">{{ __('incentive.no_participants') }}</td>
|
||||
<td colspan="{{ ($incentive->hasCashPayout() ? 11 : 10) - ($incentive->includesAbos() ? 0 : 1) }}" class="text-center text-muted">{{ __('incentive.no_participants') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@elseif ($incentive->includesAbos())
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
</div>
|
||||
|
||||
{{-- Sektion B: Kundenabo-Punkte (entfaellt bei reiner Recruiting-Challenge) --}}
|
||||
@if ($incentive->usesAbos() || $abo_sources->isNotEmpty())
|
||||
@if ($incentive->includesAbos() && ($incentive->usesAbos() || $abo_sources->isNotEmpty()))
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<strong>{{ __('incentive.section_abos') }}</strong>
|
||||
|
|
|
|||
|
|
@ -145,3 +145,21 @@ it('zeigt auf der Admin-Detailseite Export-Button und Praemienspalte nur bei Gel
|
|||
->assertOk()
|
||||
->assertDontSee('id="incentive-payout-export"', false);
|
||||
});
|
||||
|
||||
it('setzt Abo-Punkte und Mindestabos auf 0, wenn Abos nicht beruecksichtigt werden', function () {
|
||||
$this->actingAs(payoutAdmin());
|
||||
|
||||
$this->post(route('admin_incentive_store'), challengePayload([
|
||||
'include_abos' => '0', 'points_abo_onetime' => 400, 'min_customer_abos' => 6,
|
||||
]))->assertRedirect(route('admin_incentives'));
|
||||
|
||||
$incentive = Incentive::where('name', 'Recruiting-Challenge Okt-Jan')->firstOrFail();
|
||||
|
||||
expect($incentive->includesAbos())->toBeFalse()
|
||||
->and($incentive->points_abo_onetime)->toBe(0)
|
||||
->and($incentive->min_customer_abos)->toBe(0);
|
||||
|
||||
$this->get(route('admin_incentive_show', [$incentive->id]))
|
||||
->assertOk()
|
||||
->assertDontSee('<th>'.__('incentive.abos').'</th>', false);
|
||||
});
|
||||
|
|
|
|||
134
tests/Feature/Incentive/IncentiveIncludeAbosToggleTest.php
Normal file
134
tests/Feature/Incentive/IncentiveIncludeAbosToggleTest.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\Incentive;
|
||||
use App\Models\IncentiveNewAbo;
|
||||
use App\Models\IncentiveParticipant;
|
||||
use App\Models\IncentivePointsLog;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Kundenwunsch 24.09.2026: Bei der Recruiting-Challenge sollen keine Abos auftauchen.
|
||||
* Schalter "Abos beruecksichtigen" im Incentive; aus = kein Abo-Tracking.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
Carbon::setTestNow(Carbon::parse('2026-11-15 10:00:00'));
|
||||
});
|
||||
|
||||
afterEach(fn () => Carbon::setTestNow());
|
||||
|
||||
/**
|
||||
* Berater mit Eigenabo, das schon vor dem Qualifikationsstart lief.
|
||||
*
|
||||
* @return array{0: User, 1: UserAbo}
|
||||
*/
|
||||
function includeAbosConsultant(): array
|
||||
{
|
||||
$country = Country::firstOrCreate(['code' => 'DE'], [
|
||||
'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania',
|
||||
'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
$consultant = User::forceCreate([
|
||||
'email' => 'inc-abos-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
|
||||
$shoppingUser = ShoppingUser::create([
|
||||
'auth_user_id' => $consultant->id,
|
||||
'member_id' => $consultant->id,
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
'billing_email' => 'me-'.uniqid('', true).'@example.com',
|
||||
'is_from' => 'wizard',
|
||||
]);
|
||||
|
||||
$userAbo = UserAbo::create([
|
||||
'user_id' => $consultant->id,
|
||||
'member_id' => null,
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'is_for' => 'me',
|
||||
'email' => $shoppingUser->billing_email,
|
||||
'payone_userid' => 999003,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 1,
|
||||
]);
|
||||
$userAbo->forceFill(['created_at' => '2026-06-15 12:00:00'])->save();
|
||||
|
||||
return [$consultant, $userAbo];
|
||||
}
|
||||
|
||||
function includeAbosIncentive(bool $includeAbos): Incentive
|
||||
{
|
||||
return Incentive::factory()->create([
|
||||
'qualification_start' => '2026-10-01',
|
||||
'qualification_end' => '2027-01-31',
|
||||
'calculation_end' => '2027-01-31',
|
||||
'status' => 1,
|
||||
'points_partner_onetime' => 1,
|
||||
'points_abo_onetime' => 0,
|
||||
'min_direct_partners' => 0,
|
||||
'min_customer_abos' => 0,
|
||||
'include_sales_points' => false,
|
||||
'include_abos' => $includeAbos,
|
||||
]);
|
||||
}
|
||||
|
||||
it('erfasst bei ausgeschaltetem Schalter keine Abos', function () {
|
||||
[$consultant] = includeAbosConsultant();
|
||||
$incentive = includeAbosIncentive(false);
|
||||
$participant = IncentiveParticipant::factory()->create([
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $consultant->id,
|
||||
]);
|
||||
|
||||
Artisan::call('incentive:calculate', ['incentive_id' => (string) $incentive->id]);
|
||||
expect(IncentiveNewAbo::where('participant_id', $participant->id)->exists())->toBeFalse();
|
||||
|
||||
Artisan::call('incentive:calculate', ['incentive_id' => (string) $incentive->id, '--force' => true]);
|
||||
expect(IncentiveNewAbo::where('participant_id', $participant->id)->exists())->toBeFalse()
|
||||
->and($participant->fresh()->qualified_abos)->toBe(0)
|
||||
->and($incentive->usesAbos())->toBeFalse();
|
||||
});
|
||||
|
||||
it('erfasst das Eigenabo weiterhin, solange der Schalter an ist', function () {
|
||||
[$consultant, $userAbo] = includeAbosConsultant();
|
||||
$incentive = includeAbosIncentive(true);
|
||||
$participant = IncentiveParticipant::factory()->create([
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $consultant->id,
|
||||
]);
|
||||
|
||||
Artisan::call('incentive:calculate', ['incentive_id' => (string) $incentive->id]);
|
||||
|
||||
expect(IncentiveNewAbo::where('participant_id', $participant->id)->where('user_abo_id', $userAbo->id)->exists())
|
||||
->toBeTrue();
|
||||
});
|
||||
|
||||
it('raeumt bereits erfasste Abos beim naechsten Lauf ab, wenn der Schalter aus ist', function () {
|
||||
[$consultant, $userAbo] = includeAbosConsultant();
|
||||
$incentive = includeAbosIncentive(true);
|
||||
$participant = IncentiveParticipant::factory()->create([
|
||||
'incentive_id' => $incentive->id,
|
||||
'user_id' => $consultant->id,
|
||||
]);
|
||||
Artisan::call('incentive:calculate', ['incentive_id' => (string) $incentive->id]);
|
||||
expect($participant->fresh()->qualified_abos)->toBe(1);
|
||||
|
||||
$incentive->update(['include_abos' => false]);
|
||||
Artisan::call('incentive:calculate', ['incentive_id' => (string) $incentive->id]);
|
||||
|
||||
expect(IncentiveNewAbo::where('participant_id', $participant->id)->exists())->toBeFalse()
|
||||
->and(IncentivePointsLog::where('participant_id', $participant->id)->where('type', 'abo')->exists())->toBeFalse()
|
||||
->and($participant->fresh()->qualified_abos)->toBe(0);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue