turnstile

Drupal paywall plugin
Log | Files | Refs | README | LICENSE

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 }