config = $config; $this->schedule = $schedule; $this->opens = $opens; $this->issuer = $issuer; $this->mailer = $mailer; } /** * @return array{ok:bool, status:int, reason:string, door?:array, coupon?:?array, coupon_withheld?:bool, repeat?:bool, emailed?:bool} */ public function open( int $n, array $identity, \DateTimeImmutable $now ): array { $decision = $this->schedule->decide( $n, $now, $this->config['missed'] ); if ( ! $decision['allowed'] ) { return self::refuse( 'invalid' === $decision['reason'] ? 404 : 403, $decision['reason'] ); } $mode = $this->config['mode']; $subscriber = $identity['subscriber'] ?? null; $userEmail = $identity['user_email'] ?? null; if ( 'email' === $mode && empty( $subscriber['id'] ) ) { return self::refuse( 403, 'need_email' ); } if ( 'login' === $mode && ( empty( $subscriber['id'] ) || empty( $userEmail ) ) ) { return self::refuse( 403, 'need_login' ); } $door = $this->config['door_list'][ $n ]; $hasCoupon = 'content' !== $door['type']; $wantCoupon = $hasCoupon && $decision['coupon']; $result = [ 'ok' => true, 'status' => 200, 'reason' => $decision['reason'], 'door' => self::payload( $door, $n ), 'coupon' => null, 'coupon_withheld' => $hasCoupon && ! $decision['coupon'], 'repeat' => false, 'emailed' => false, ]; if ( 'open' === $mode ) { $coupon = $wantCoupon ? $this->issuer->shared( $door, $n ) : null; $result['coupon'] = $coupon; $this->opens->record( null, $n, $coupon ? $coupon['id'] : null ); return $result; } $subscriberId = (int) $subscriber['id']; $existing = $this->opens->find( $subscriberId, $n ); if ( null !== $existing ) { return $this->repeat( $result, $existing ); } $email = 'login' === $mode ? $userEmail : $subscriber['email']; $coupon = $wantCoupon ? $this->issuer->issue( $door, $n, $email ) : null; if ( ! $this->opens->record( $subscriberId, $n, $coupon ? $coupon['id'] : null ) ) { // A parallel request opened this door first; its coupon is the real one. if ( $coupon ) { $this->issuer->discard( $coupon['id'] ); } return $this->repeat( $result, $this->opens->find( $subscriberId, $n ) ); } $result['coupon'] = $coupon; if ( $coupon && 'email' === $mode ) { $result['emailed'] = $this->mail( $email, $coupon, $n ); } return $result; } private function repeat( array $result, ?array $existing ): array { $couponId = $existing['coupon_id'] ?? null; $result['repeat'] = true; $result['coupon'] = $couponId ? $this->issuer->find( (int) $couponId ) : null; return $result; } /** * A failed email must not cost the visitor a coupon they can already see. */ private function mail( string $email, array $coupon, int $n ): bool { try { $this->mailer->sendCoupon( $email, $coupon, $n ); return true; } catch ( \Throwable $e ) { return false; } } private static function payload( array $door, int $n ): array { return [ 'number' => $n, 'type' => $door['type'], 'content' => 'coupon' === $door['type'] ? null : $door['content'], ]; } private static function refuse( int $status, string $reason ): array { return [ 'ok' => false, 'status' => $status, 'reason' => $reason, ]; } }