TurnstilePriceCategory.php (11311B)
1 <?php 2 3 /** 4 * @file 5 * Price category structure for the GNU Taler Turnstile module. 6 */ 7 8 namespace Drupal\taler_turnstile\Entity; 9 10 use Drupal\Core\Config\Entity\ConfigEntityBase; 11 use Drupal\Core\Entity\EntityStorageInterface; 12 use Drupal\Core\StringTranslation\StringTranslationTrait; 13 14 /** 15 * Defines the Price Category entity. 16 * 17 * @ConfigEntityType( 18 * id = "taler_turnstile_price_category", 19 * label = @Translation("Price Category"), 20 * handlers = { 21 * "list_builder" = "Drupal\taler_turnstile\PriceCategoryListBuilder", 22 * "form" = { 23 * "add" = "Drupal\taler_turnstile\Form\PriceCategoryForm", 24 * "edit" = "Drupal\taler_turnstile\Form\PriceCategoryForm", 25 * "delete" = "Drupal\taler_turnstile\Form\PriceCategoryDeleteForm" 26 * } 27 * }, 28 * config_prefix = "taler_turnstile_price_category", 29 * admin_permission = "administer price categories", 30 * entity_keys = { 31 * "id" = "id", 32 * "label" = "label" 33 * }, 34 * links = { 35 * "collection" = "/admin/structure/taler-turnstile-price-categories", 36 * "edit-form" = "/admin/structure/taler-turnstile-price-categories/{price_category}/edit", 37 * "delete-form" = "/admin/structure/taler-turnstile-price-categories/{price_category}/delete" 38 * }, 39 * config_export = { 40 * "id", 41 * "label", 42 * "description", 43 * "prices" 44 * } 45 * ) 46 */ 47 class TurnstilePriceCategory extends ConfigEntityBase { 48 49 /** 50 * For i18n, gives us the t() function. 51 */ 52 use StringTranslationTrait; 53 54 /** 55 * The price category ID. 56 * 57 * @var string 58 */ 59 protected $id; 60 61 /** 62 * The price category label. 63 * 64 * @var string 65 */ 66 protected $label; 67 68 /** 69 * The price category description. 70 * 71 * @var string 72 */ 73 protected $description; 74 75 /** 76 * The prices array. 77 * 78 * Structure: ['subscription_id' => ['currency_code' => 'price']] 79 * 80 * @var array 81 */ 82 protected $prices = []; 83 84 /** 85 * Gets the description. 86 * 87 * @return string 88 * The description. 89 */ 90 public function getDescription() { 91 return $this->description; 92 } 93 94 /** 95 * Compute the merchant template ID used to publish the prices 96 * of this category in the merchant backend. Kept short and 97 * reasonably distinctive (per the README, "turnstile-$NAME"). 98 * 99 * @return string 100 * Template ID 101 */ 102 public function getTemplateId(): string { 103 return 'turnstile-' . $this->id(); 104 } 105 106 107 /** 108 * Return the set of "currency:amount" strings that match valid 109 * payment amounts for this category. Used to verify that a paid 110 * contract actually paid for the right thing. 111 * 112 * @param array $subscriptions 113 * Live subscription data, as produced by 114 * TalerMerchantApiService::getSubscriptions(). 115 * @return array<string> 116 */ 117 public function getAcceptableAmounts(array $subscriptions): array { 118 $amounts = []; 119 foreach ($this->getPaymentChoices($subscriptions) as $choice) { 120 if (isset($choice['amount'])) { 121 $amounts[] = $choice['amount']; 122 } 123 } 124 return $amounts; 125 } 126 127 128 /** 129 * Gets a brief hint to display about non-subscriber prices. 130 * 131 * @return string 132 * A hint about the price 133 */ 134 public function getPriceHint() { 135 $prices = $this->getPrices(); 136 $nosub = $prices['%none%']; 137 $rval = NULL; 138 foreach ($nosub as $currency => $price) { 139 if (NULL === $rval) 140 { 141 $rval = "$price $currency"; 142 } 143 else 144 { 145 $rval = "$price $currency, " . $rval; 146 } 147 } 148 return $rval ?? $this->t(/* Need to buy a subscription */ 'Not sold individually'); 149 } 150 151 /** 152 * Gets a brief hint to display about applicable subscriptions. 153 * 154 * @return string 155 * A hint about subscriptions 156 */ 157 public function getSubscriptionHint() { 158 $prices = $this->getPrices(); 159 $rval = NULL; 160 foreach ($prices as $subscription => $details) { 161 if ('%none%' === $subscription) 162 continue; 163 if (NULL === $rval) 164 { 165 $rval = $subscription; 166 } 167 else 168 { 169 $rval = $subscription . ", " . $rval; 170 } 171 } 172 return $rval ?? $this->t(/* No subscriptions */ 'None'); 173 } 174 175 /** 176 * Gets all prices. 177 * 178 * @return array 179 * The prices array. 180 */ 181 public function getPrices() { 182 return $this->prices ?: []; 183 } 184 185 /** 186 * Return the different subscriptions that this price category 187 * has for which the resulting payment amount is zero (thus, 188 * exlude subscriptions that would merely yield a discount). 189 * 190 * @return array 191 * The names (slugs) of the subscriptions 192 * that for this price category would yield a price of zero; 193 * note that empty prices do NOT count as zero but infinity! 194 */ 195 public function getFullSubscriptions(): array { 196 $subscriptions = []; 197 foreach ($this->getPrices() as $tokenFamilySlug => $currencyMap) { 198 foreach ($currencyMap as $currencyCode => $price) { 199 if ( (is_numeric ($price)) && 200 (0.0 == (float) $price) ) { 201 $subscriptions[] = $tokenFamilySlug; 202 break; 203 } 204 } 205 } 206 return $subscriptions; 207 } 208 209 /** 210 * Return the different payment choices in a way suitable 211 * for GNU Taler v1 contracts. 212 * 213 * @param array $expirations 214 * Times when each possible subscription type expires. 215 * @return array 216 * Structure suitable for the choices array in the v1 contract 217 */ 218 public function getPaymentChoices(array $subscriptions): array { 219 $max_cache = time() + 3600; 220 $choices = []; 221 222 foreach ($this->getPrices() as $tokenFamilySlug => $currencyMap) { 223 if ("%none%" !== $tokenFamilySlug) { 224 $subscription = $subscriptions[$tokenFamilySlug]; 225 $expi = $subscription['valid_before_s'] ?? 0; 226 if ($expi < time()) { 227 \Drupal::logger('taler_turnstile')->info('Subscription category @slug expired at @expire, skipping it.', ['@slug' => $tokenFamilySlug, '@expire' => $expi]); 228 continue; // already expired 229 } 230 $max_cache = min ($max_cache, 231 $expi); 232 } 233 foreach ($currencyMap as $currencyCode => $price) { 234 $inputs = []; 235 if ("%none%" !== $tokenFamilySlug) 236 { 237 $inputs[] = [ 238 'type' => 'token', 239 'token_family_slug' => $tokenFamilySlug, 240 'count' => 1, 241 ]; 242 $description = $this->t('Pay in @currency with subscription', [ 243 '@currency' => $currencyCode, 244 ], [ 245 'langcode' => 'en', // force English version here! 246 ]); 247 $description_i18n = $this->buildTranslationMap ( 248 'Pay in @currency with subscription', 249 ['@currency' => $currencyCode] 250 ); 251 $choices[] = [ 252 'amount' => $currencyCode . ':' . $price, 253 'description' => 'Pay in ' . $currencyCode . ' with subscription', 254 // 'description_i18n' => $description_i18n, 255 'inputs' => $inputs, 256 ]; 257 $subscription_price = $this->getSubscriptionPrice ($tokenFamilySlug, $currencyCode); 258 if ($subscription_price !== NULL) { 259 // This subscription can be bought. 260 $outputs = []; 261 $outputs[] = [ 262 'type' => 'token', 263 'token_family_slug' => $tokenFamilySlug, 264 'count' => 1, 265 ]; 266 $description = $this->t('Buy subscription in @currency', [ 267 '@currency' => $currencyCode, 268 ]); 269 $description_i18n = $this->buildTranslationMap ( 270 'Buy subscription in @currency', 271 ['@currency' => $currencyCode] 272 ); 273 $choices[] = [ 274 'amount' => $currencyCode . ':' . ((float) $subscription_price + 275 (float) $price), 276 'description' => $description, 277 'description_i18n' => $description_i18n, 278 'outputs' => $outputs, 279 ]; 280 } 281 } 282 else // case for no subscription 283 { 284 $description = $this->t('Pay in @currency', [ 285 '@currency' => $currencyCode, 286 ]); 287 $description_i18n = $this->buildTranslationMap ( 288 'Pay in @currency', 289 ['@currency' => $currencyCode] 290 ); 291 $choices[] = [ 292 'amount' => $currencyCode . ':' . (float) $price, 293 'description' => $description, 294 'description_i18n' => $description_i18n, 295 'inputs' => $inputs, 296 ]; 297 } 298 } // for each possible currency 299 } // for each type of subscription 300 301 return $choices; 302 } 303 304 /** 305 * Sets the prices array. 306 * 307 * @param array $prices 308 * The prices array. 309 * 310 * @return $this 311 */ 312 public function setPrices(array $prices) { 313 $this->prices = $prices; 314 return $this; 315 } 316 317 /** 318 * Determine the price of the given type of subscription 319 * in the given currency. 320 * 321 * @param string $tokenFamilySlug 322 * The slug of the token family 323 * @param string $currencyCode 324 * Currency code in which a price quote is desired 325 * 326 * @return string|null 327 * The subscription price (will map to a float), NULL on error 328 */ 329 private function getSubscriptionPrice (string $tokenFamilySlug, string $currencyCode) { 330 $config = \Drupal::config('taler_turnstile.settings'); 331 $subscriptions_prices = $config->get('subscription_prices') ?? []; 332 $subscription_prices = $subscriptions_prices[$tokenFamilySlug] ?? []; 333 $subscription_price = $subscription_prices[$currencyCode] ?? NULL; 334 return $subscription_price; 335 } 336 337 338 /** 339 * {@inheritdoc} 340 * 341 * Mirror this category as a "paivana"-style template in the 342 * merchant backend on every save (create or update). 343 */ 344 public function postSave(EntityStorageInterface $storage, $update = TRUE) { 345 parent::postSave($storage, $update); 346 /** @var \Drupal\taler_turnstile\TalerMerchantApiService $api */ 347 $api = \Drupal::service('taler_turnstile.api_service'); 348 $api->syncTemplate($this); 349 } 350 351 352 /** 353 * {@inheritdoc} 354 * 355 * Remove the matching template from the merchant backend when 356 * this category goes away. 357 */ 358 public static function postDelete(EntityStorageInterface $storage, array $entities) { 359 parent::postDelete($storage, $entities); 360 /** @var \Drupal\taler_turnstile\TalerMerchantApiService $api */ 361 $api = \Drupal::service('taler_turnstile.api_service'); 362 foreach ($entities as $entity) { 363 /** @var TurnstilePriceCategory $entity */ 364 $api->deleteTemplate($entity->getTemplateId()); 365 } 366 } 367 368 369 /** 370 * Build a translation map for all enabled languages. 371 * 372 * @param string $string 373 * The translatable string. 374 * @param array $args 375 * Placeholder replacements. 376 * 377 * @return array 378 * Map of language codes to translated strings. 379 */ 380 private function buildTranslationMap(string $string, array $args = []): array { 381 $translations = []; 382 $language_manager = \Drupal::languageManager(); 383 384 foreach ($language_manager->getLanguages() as $langcode => $language) { 385 $translation = $this->t($string, $args, [ 386 'langcode' => $langcode, 387 ]); 388 $translations[$langcode] = (string) $translation; 389 } 390 return $translations; 391 } 392 393 }