Abo: Bestellung extern als erledigt markieren (ohne Rechnung/Inkasso)

Neue Admin-Funktion fuer angehaltene Abo-Bestellungen, die der Kunde
ausserhalb des Systems geliefert und verrechnet hat.

Aktion (AboExternalCompletionService):
- Punkte gutschreiben wie bei eingegangener Zahlung (SalesPointsVolume,
  idempotent gegen Doppelbuchung)
- UserAboOrder auf neuen gruenen Status 8 "extern erledigt"; paid bleibt false
- Bestellung: shipped=2 (Lieferung bestaetigt), keine Rechnung/kein Lieferschein
- Blockade aufheben: Abo zurueck auf status=2 (okay), aktiv, next_date neu,
  damit der naechste Termin regulaer laeuft
- wie bei regulaerer Ausfuehrung: IncentiveTracker + AboOneTimeService-Purge

Einstiegspunkte: Button + Bestaetigungs-Modal auf der Kunden-Bestelldetailseite
(SalesController) und auf der Abo-Detailseite (Admin\AboController@externDone,
Route admin_abos_extern_done). Status 8 (gruen) in UserAbo/UserAboOrder, Labels
de/en.

Tests: tests/Feature/AboExternalCompletionTest (Happy Path, Idempotenz,
bezahlt-/Nicht-Abo-Guards, Abo-Detail-Einstieg) - 5 gruen.

Zusaetzlich: docs/2026-06-11-test-suite-vorbestehende-fehler.md - Diagnose der
93 vorbestehenden (von master geerbten) Test-Suite-Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-06-11 15:40:16 +00:00
parent 8593dbe65d
commit 090debf1b4
13 changed files with 609 additions and 59 deletions

View file

@ -6,7 +6,6 @@
namespace App\Models;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
@ -21,7 +20,7 @@ use Illuminate\Database\Eloquent\Model;
* @property Carbon|null $updated_at
* @property ShoppingOrder $shopping_order
* @property UserAbo $user_abo
* @package App\Models
*
* @method static \Illuminate\Database\Eloquent\Builder|UserAboOrder newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserAboOrder newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserAboOrder query()
@ -31,73 +30,74 @@ use Illuminate\Database\Eloquent\Model;
* @method static \Illuminate\Database\Eloquent\Builder|UserAboOrder whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserAboOrder whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserAboOrder whereUserAboId($value)
*
* @mixin \Eloquent
*/
class UserAboOrder extends Model
{
protected $table = 'user_abo_orders';
protected $table = 'user_abo_orders';
protected $casts = [
'user_abo_id' => 'int',
'shopping_order_id' => 'int',
'status' => 'int',
'paid' => 'bool'
];
protected $casts = [
'user_abo_id' => 'int',
'shopping_order_id' => 'int',
'status' => 'int',
'paid' => 'bool',
];
protected $fillable = [
'user_abo_id',
'user_id',
'shopping_order_id',
'status',
'paid'
];
protected $fillable = [
'user_abo_id',
'user_id',
'shopping_order_id',
'status',
'paid',
];
public static $statusTypes = [
0 => 'abo_new',
1 => 'abo_new',
2 => 'abo_okay',
3 => 'abo_hold',
4 => 'abo_cancel',
5 => 'abo_finish',
6 => 'abo_inactive',
7 => 'abo_grace'
];
public static $statusTypes = [
0 => 'abo_new',
1 => 'abo_new',
2 => 'abo_okay',
3 => 'abo_hold',
4 => 'abo_cancel',
5 => 'abo_finish',
6 => 'abo_inactive',
7 => 'abo_grace',
8 => 'abo_extern_done',
];
public static $statusColors = [
0 => 'success',
1 => 'success',
2 => 'secondary',
3 => 'warning',
4 => 'danger',
5 => 'info',
6 => 'warning',
7 => 'danger'
];
public static $statusColors = [
0 => 'success',
1 => 'success',
2 => 'secondary',
3 => 'warning',
4 => 'danger',
5 => 'info',
6 => 'warning',
7 => 'danger',
8 => 'success',
];
public function shopping_order()
{
return $this->belongsTo(ShoppingOrder::class);
}
public function user_abo()
{
return $this->belongsTo(UserAbo::class);
}
public function shopping_order()
{
return $this->belongsTo(ShoppingOrder::class);
}
public function getStatusFormated()
{
return '<span class="badge badge-pill badge-'.$this->getStatusColor().'">'.$this->getStatusType().'</span>';
}
public function user_abo()
{
return $this->belongsTo(UserAbo::class);
}
public function getStatusType()
{
return isset(self::$statusTypes[$this->status]) ? __('abo.'.self::$statusTypes[$this->status]) : '';
}
public function getStatusFormated()
{
return '<span class="badge badge-pill badge-' . $this->getStatusColor() . '">' . $this->getStatusType() . '</span>';
}
public function getStatusType()
{
return isset(self::$statusTypes[$this->status]) ? __('abo.' . self::$statusTypes[$this->status]) : "";
}
public function getStatusColor()
{
return isset(self::$statusColors[$this->status]) ? self::$statusColors[$this->status] : "default";
}
public function getStatusColor()
{
return isset(self::$statusColors[$this->status]) ? self::$statusColors[$this->status] : 'default';
}
}