1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Models\Store;
7
8use App\Exceptions\InvariantException;
9use App\Exceptions\ValidationException;
10use App\Libraries\ChangeUsername;
11use App\Models\SupporterTag;
12use App\Traits\Validatable;
13use Illuminate\Database\Eloquent\SoftDeletes;
14
15/**
16 * @property float|null $cost
17 * @property \Carbon\Carbon $created_at
18 * @property \Carbon\Carbon|null $deleted_at
19 * @property ExtraDataBase|null $extra_data
20 * @property string|null $extra_info
21 * @property int $id
22 * @property Order $order
23 * @property int $order_id
24 * @property Product $product
25 * @property int $product_id
26 * @property int $quantity
27 * @property bool $reserved
28 * @property \Carbon\Carbon|null $updated_at
29 */
30class OrderItem extends Model
31{
32 use SoftDeletes, Validatable;
33
34 protected $primaryKey = 'id';
35
36 protected $casts = [
37 'cost' => 'float',
38 'extra_data' => ExtraDataBase::class,
39 'reserved' => 'boolean',
40 ];
41
42 public function scopeHasShipping($query)
43 {
44 return $query->whereHas('product', function ($q) {
45 return $q->hasShipping();
46 });
47 }
48
49 public function isValid()
50 {
51 $this->validationErrors()->reset();
52
53 if ($this->quantity < 0) {
54 $this->validationErrors()->add('quantity', 'not_negative');
55 }
56
57 if ($this->cost < 0) {
58 $this->validationErrors()->add('cost', 'not_negative');
59 }
60
61 return $this->validationErrors()->isEmpty();
62 }
63
64 public function delete()
65 {
66 if (!$this->order->isCart()) {
67 throw new InvariantException("Delete not allowed on Order ({$this->order->getKey()}).");
68 }
69
70 parent::delete();
71 }
72
73 public function save(array $options = [])
74 {
75 $skipValidations = $options['skipValidations'] ?? false;
76 if (!$skipValidations && !$this->isValid()) {
77 // FIXME: Simpler to just throw instead of fixing all the save() calls right now.
78 throw new ValidationException($this->validationErrors());
79 }
80
81 return parent::save($options);
82 }
83
84 public function subtotal()
85 {
86 return $this->cost * $this->quantity;
87 }
88
89 public function order()
90 {
91 return $this->belongsTo(Order::class, 'order_id');
92 }
93
94 public function product()
95 {
96 return $this->belongsTo(Product::class, 'product_id');
97 }
98
99 public function scopeCustomClass($query, $name)
100 {
101 return $query->whereHas('product', function ($q) use ($name) {
102 $q->customClass($name);
103 });
104 }
105
106 public function refreshCost()
107 {
108 if ($this->product->cost === null) {
109 return;
110 }
111 $this->cost = $this->product->cost;
112 }
113
114 public function getCustomClassInstance()
115 {
116 // only one for now
117 if ($this->product->custom_class === 'username-change') {
118 return new ChangeUsername($this->order->user, $this->extra_info ?? '');
119 }
120 }
121
122 public function getDisplayName(bool $html = false)
123 {
124 switch ($this->product->custom_class) {
125 case Product::SUPPORTER_TAG_NAME:
126 return SupporterTag::getDisplayName($this, $html);
127 default:
128 return $this->product->name.($this->extra_info !== null ? " ({$this->extra_info})" : '');
129 }
130 }
131
132 public function getSubtext()
133 {
134 $extraData = $this->extra_data;
135 if ($extraData instanceof ExtraDataSupporterTag && $extraData->message !== null) {
136 return trans('store.order.item.subtext.supporter_tag', ['message' => $extraData->message]);
137 }
138
139 return null;
140 }
141
142 public function releaseProduct()
143 {
144 if ($this->reserved) {
145 $this->product->release($this->quantity);
146 $this->reserved = false;
147 $this->saveOrExplode();
148 }
149 }
150
151 public function reserveProduct()
152 {
153 if (!$this->reserved) {
154 $this->product->reserve($this->quantity);
155 $this->reserved = true;
156 $this->saveOrExplode();
157 }
158 }
159
160 public function validationErrorsTranslationPrefix(): string
161 {
162 return 'store.order_item';
163 }
164
165 public function __get($key)
166 {
167 // TODO: remove this after no more things are queued with old $casts
168 $this->casts['extra_data'] = ExtraDataBase::class;
169
170 return parent::__get($key);
171 }
172}