add support for storing custom metadata to cart/order items and options
[bse.git] / site / cgi-bin / modules / BSE / TB / Product.pm
CommitLineData
10dd37f9 1package BSE::TB::Product;
15fb10f2 2use strict;
41b9d8ec 3# represents a product from the database
e0ed81d7 4use BSE::TB::Articles;
41b9d8ec 5use vars qw/@ISA/;
e0ed81d7 6@ISA = qw/BSE::TB::Article/;
41b9d8ec 7
33d04a1b 8our $VERSION = "1.006";
cb7fd78d 9
af74f0b4
TC
10# subscription_usage values
11use constant SUBUSAGE_START_ONLY => 1;
12use constant SUBUSAGE_RENEW_ONLY => 2;
13use constant SUBUSAGE_EITHER => 3;
14
41b9d8ec
TC
15sub columns {
16 return ($_[0]->SUPER::columns(),
74b21f6d 17 qw/articleId description leadTime retailPrice wholesalePrice gst options
0ec4ac8a 18 subscription_id subscription_period subscription_usage
306eb97a 19 subscription_required product_code weight length height width/ );
41b9d8ec
TC
20}
21
22sub bases {
e0ed81d7 23 return { articleId=>{ class=>'BSE::TB::Article'} };
41b9d8ec
TC
24}
25
0ec4ac8a
TC
26sub subscription_required {
27 my ($self) = @_;
28
29 my $id = $self->{subscription_required};
30 return if $id == -1;
31
32 require BSE::TB::Subscriptions;
33 return BSE::TB::Subscriptions->getByPkey($id);
34}
35
4175638b
TC
36sub subscription {
37 my ($self) = @_;
38
39 my $id = $self->{subscription_id};
40 return if $id == -1;
41
42 require BSE::TB::Subscriptions;
43 return BSE::TB::Subscriptions->getByPkey($id);
44}
45
af74f0b4
TC
46sub is_renew_sub_only {
47 my ($self) = @_;
48
49 $self->{subscription_usage} == SUBUSAGE_RENEW_ONLY;
50}
51
52sub is_start_sub_only {
53 my ($self) = @_;
54
55 $self->{subscription_usage} == SUBUSAGE_START_ONLY;
56}
57
58baa27b
TC
58sub _get_cfg_options {
59 my ($cfg) = @_;
0eb78304
TC
60
61 require BSE::CfgInfo;
62 my $avail_options = BSE::CfgInfo::product_options($cfg);
58baa27b
TC
63 my @options;
64 for my $name (keys %$avail_options) {
65 my $rawopt = $avail_options->{$name};
66 my %opt =
67 (
68 id => $name,
69 name => $rawopt->{desc},
70 default => $rawopt->{default} || '',
71 );
72 my @values;
73 for my $value (@{$rawopt->{values}}) {
74 my $label = $rawopt->{labels}{$value} || $value;
75 push @values,
76 bless
77 {
78 id => $value,
79 value => $label,
80 }, "BSE::CfgProductOptionValue";
81 }
82 $opt{values} = \@values;
83 push @options, bless \%opt, "BSE::CfgProductOption";
84 }
85
86 return @options;
87}
88
89sub _get_prod_options {
90 my ($product, $cfg, @values) = @_;
91
92 my %all_cfg_opts = map { $_->id => $_ } _get_cfg_options($cfg);
0eb78304 93 my @opt_names = split /,/, $product->{options};
58baa27b
TC
94
95 my @cfg_opts = map $all_cfg_opts{$_}, @opt_names;
96 my @db_opts = grep $_->enabled, $product->db_options;
97 my @all_options = ( @cfg_opts, @db_opts );
98
99 push @values, '' while @values < @all_options;
100
101 my $index = 0;
102 my @sem_options;
103 for my $opt (@all_options) {
104 my @opt_values = $opt->values;
105 my %opt_values = map { $_->id => $_->value } @opt_values;
106 my $result_opt =
107 {
108 id => $opt->key,
109 name => $opt->key,
110 desc => $opt->name,
111 value => $values[$index],
112 type => $opt->type,
113 labels => \%opt_values,
114 default => $opt->default_value,
115 };
116 my $value = $values[$index];
117 if (defined $value) {
118 $result_opt->{values} = [ map $_->id, @opt_values ],
119 $result_opt->{display} = $opt_values{$values[$index]};
120 }
121 push @sem_options, $result_opt;
122 ++$index;
0eb78304
TC
123 }
124
125 return @sem_options;
126}
127
128sub option_descs {
129 my ($self, $cfg, $rvalues) = @_;
130
131 $rvalues or $rvalues = [ ];
132
133 return $self->_get_prod_options($cfg, @$rvalues);
134}
135
58baa27b
TC
136sub db_options {
137 my ($self) = @_;
138
139 require BSE::TB::ProductOptions;
66371e15
TC
140 return BSE::TB::ProductOptions->getBy2
141 (
142 [ product_id => $self->{id} ],
143 { order => "display_order" }
144 );
58baa27b
TC
145}
146
726ffaed
TC
147sub remove {
148 my ($self, $cfg) = @_;
149
150 # remove any product options
151 for my $opt ($self->db_options) {
152 $opt->remove;
153 }
154
155 # mark any order line items to "anonymize" them
156 BSE::DB->run(bseMarkProductOrderItemsAnon => $self->id);
157
158 # remove any wishlist items
159 BSE::DB->run(bseRemoveProductFromWishlists => $self->id);
160
dfd483db
TC
161 # remove any tiered prices
162 BSE::DB->run(bseRemoveProductPrices => $self->id);
163
726ffaed
TC
164 return $self->SUPER::remove($cfg);
165}
166
1d383001
TC
167sub has_sale_files {
168 my ($self) = @_;
169
170 my ($row) = BSE::DB->query(bseProductHasSaleFiles => $self->{id});
171
172 return $row->{have_sale_files};
173}
174
dfd483db
TC
175sub prices {
176 my ($self) = @_;
177
178 require BSE::TB::PriceTierPrices;
179 my @prices = BSE::TB::PriceTierPrices->getBy(product_id => $self->id);
180}
181
182=item set_prices($prices)
183
184Set tiered pricing for the product.
185
186I<$prices> is a hashref mapping tier ids to prices in cents.
187
188If a tier doesn't have a price in I<$prices> it's removed from the
189product.
190
191=cut
192
193sub set_prices {
194 my ($self, $prices) = @_;
195
196 my %current = map { $_->tier_id => $_ } $self->prices;
197 for my $tier_id (keys %$prices) {
198 my $current = delete $current{$tier_id};
199 if ($current) {
200 $current->set_retailPrice($prices->{$tier_id});
201 $current->save;
202 }
203 else {
204 BSE::TB::PriceTierPrices->make
205 (
206 tier_id => $tier_id,
207 product_id => $self->id,
208 retailPrice => $prices->{$tier_id},
209 );
210 }
211 }
212
213 # remove any spares
214 for my $price (values %current) {
215 $price->remove;
216 }
217}
218
219=item price(user => $user, date => $sql_date)
220
221=item price(user => $user)
222
223Return the retail price depending on the user and date
224and optionally the tier object (in list context).
225
226If no tier matches then the undef is returned at the tier object.
227
228=cut
229
230sub price {
231 my ($self, %opts) = @_;
232
233 my $user = delete $opts{user};
234 my $date = delete $opts{date} || BSE::Util::SQL::now_sqldate();
10dd37f9 235 my @tiers = BSE::TB::Products->pricing_tiers;
dfd483db
TC
236 my %prices = map { $_->tier_id => $_ } $self->prices;
237
238 my $price;
239 my $found_tier;
240 for my $tier (@tiers) {
241 if ($prices{$tier->id}
242 && $tier->match($user, $date)) {
243 $price = $prices{$tier->id}->retailPrice;
244 $found_tier = $tier;
245 last;
246 }
247 }
248
249 defined $price or $price = $self->retailPrice;
250
251 return wantarray ? ( $price, $found_tier ) : $price;
252}
253
254sub update_dynamic {
255 my ($self, $cfg) = @_;
256
10dd37f9 257 my @tiers = BSE::TB::Products->pricing_tiers;
dfd483db
TC
258 if (@tiers) {
259 $self->set_cached_dynamic(1);
260 return;
261 }
262
263 return $self->SUPER::update_dynamic($cfg);
264}
265
b6a28bd1 266sub tableClass {
10dd37f9 267 return "BSE::TB::Products";
b6a28bd1
TC
268}
269
58baa27b
TC
270package BSE::CfgProductOption;
271use strict;
272
273sub id { $_[0]{id} }
274
275sub key {$_[0]{id} } # same as id for config options
276
277sub type { "select" }
278
279sub name { $_[0]{name} }
280
281sub values {
282 @{$_[0]{values}}
283}
284
285sub default_value { $_[0]{default} }
286
287package BSE::CfgProductOptionValue;
288use strict;
289
290sub id { $_[0]{id} }
291
292sub value { $_[0]{value} }
293
33d04a1b
TC
294sub get_custom { undef }
295
41b9d8ec 2961;