-
Notifications
You must be signed in to change notification settings - Fork 0
chore: Release 0.2.0 #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
79df406
6daf455
d679eba
f67b391
538d78e
9ba3db7
bbde29c
0339878
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <?php | ||
|
|
||
| namespace Whilesmart\Admin\Contracts; | ||
|
|
||
| /** | ||
| * One input a provider needs in order to create an offer. | ||
| */ | ||
| final class OfferField | ||
| { | ||
| /** | ||
| * @param 'text'|'number'|'date'|'select'|'boolean' $type | ||
| * @param array<int|string, string> $options For 'select', value => label. | ||
| */ | ||
| public function __construct( | ||
| public readonly string $name, | ||
| public readonly string $label, | ||
| public readonly string $type = 'text', | ||
| public readonly bool $required = false, | ||
| public readonly ?string $help = null, | ||
| public readonly array $options = [], | ||
| ) {} | ||
|
|
||
| /** @return array<string, mixed> */ | ||
| public function toArray(): array | ||
| { | ||
| return [ | ||
| 'name' => $this->name, | ||
| 'label' => $this->label, | ||
| 'type' => $this->type, | ||
| 'required' => $this->required, | ||
| 'help' => $this->help, | ||
| 'options' => $this->options, | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <?php | ||
|
|
||
| namespace Whilesmart\Admin\Contracts; | ||
|
|
||
| use Whilesmart\Admin\Support\Offer; | ||
|
|
||
| /** | ||
| * Lists and creates a host's discounts without knowing what stores them. | ||
| */ | ||
| interface OfferProvider | ||
| { | ||
| /** Stable machine key, unique across registered providers. */ | ||
| public function key(): string; | ||
|
|
||
| /** Human label, shown as the section heading. */ | ||
| public function label(): string; | ||
|
|
||
| /** | ||
| * The inputs create() expects, which the console renders as a form. | ||
| * | ||
| * @return array<int, OfferField> | ||
| */ | ||
| public function fields(): array; | ||
|
|
||
| /** | ||
| * @return Offer[] | ||
| */ | ||
| public function all(): array; | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $attributes Keyed by the field names above. | ||
| */ | ||
| public function create(array $attributes): Offer; | ||
|
|
||
| /** Stop an offer being redeemed again. Redemptions already made stand. */ | ||
| public function revoke(string $id): void; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,9 +6,12 @@ | |||||||||||||||||||||||||||||||||||||||
| use Illuminate\Http\Request; | ||||||||||||||||||||||||||||||||||||||||
| use Illuminate\Routing\Controller; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Contracts\AdminUserProvider; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Contracts\OfferProvider; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Http\Requests\CreateOfferRequest; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Http\Resources\AdminUserResource; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Mail\TemplateMail; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Models\MailTemplate; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Support\OfferRegistry; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Admin\Support\TemplateRegistry; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Engagement\EngagementManager; | ||||||||||||||||||||||||||||||||||||||||
| use Whilesmart\Engagement\Support\ClientRegistry; | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,6 +20,74 @@ | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class AdminController extends Controller | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Every registered provider, its fields, and its offers. | ||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||
| * A host with none registered gets an empty list, not an error. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| public function offers(Request $request, OfferRegistry $offers): JsonResponse | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| $this->authorizeOffers($request); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return response()->json([ | ||||||||||||||||||||||||||||||||||||||||
| 'success' => true, | ||||||||||||||||||||||||||||||||||||||||
| 'data' => array_values(array_map(fn ($provider) => [ | ||||||||||||||||||||||||||||||||||||||||
| 'key' => $provider->key(), | ||||||||||||||||||||||||||||||||||||||||
| 'label' => $provider->label(), | ||||||||||||||||||||||||||||||||||||||||
| 'fields' => array_map(fn ($field) => $field->toArray(), $provider->fields()), | ||||||||||||||||||||||||||||||||||||||||
| 'offers' => array_map(fn ($offer) => $offer->toArray(), $provider->all()), | ||||||||||||||||||||||||||||||||||||||||
| ], $offers->all())), | ||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public function createOffer(Request $request, OfferRegistry $offers, string $provider): JsonResponse | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| $this->authorizeOffers($request); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Defaulted rather than read straight out: a host that published this | ||||||||||||||||||||||||||||||||||||||||
| // config before the key existed has an array that wins over the one | ||||||||||||||||||||||||||||||||||||||||
| // shipped here, and would resolve nothing. | ||||||||||||||||||||||||||||||||||||||||
| $requestClass = config('admin.requests.create_offer', CreateOfferRequest::class); | ||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The shared write path validates only the shape of the payload (
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| $validated = app($requestClass)->validated(); | ||||||||||||||||||||||||||||||||||||||||
| $offer = $this->offerProvider($offers, $provider)->create($validated['attributes']); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return response()->json(['success' => true, 'data' => $offer->toArray()], 201); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public function revokeOffer(Request $request, OfferRegistry $offers, string $provider, string $id): JsonResponse | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| $this->authorizeOffers($request); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| $this->offerProvider($offers, $provider)->revoke($id); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return response()->json(['success' => true]); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Offers answer to the configured owner, the way a mail template answers to | ||||||||||||||||||||||||||||||||||||||||
| * its own. Checked here rather than in the request, so a host swapping the | ||||||||||||||||||||||||||||||||||||||||
| * request class cannot drop it. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| private function authorizeOffers(Request $request): void | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| $owner = config('admin.owner'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| abort_unless(app(OwnerAuthorizer::class)->authorize( | ||||||||||||||||||||||||||||||||||||||||
| $request->user(), | ||||||||||||||||||||||||||||||||||||||||
| $owner['type'], | ||||||||||||||||||||||||||||||||||||||||
| $owner['id'], | ||||||||||||||||||||||||||||||||||||||||
| ), 403); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| private function offerProvider(OfferRegistry $offers, string $key): OfferProvider | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| $provider = $offers->get($key); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| abort_if($provider === null, 404, 'No offer provider is registered under that key.'); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return $provider; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| public function metrics(Request $request, EngagementManager $engagement, ClientRegistry $clients): JsonResponse | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| $granularity = in_array($request->query('granularity'), ['day', 'week', 'month'], true) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?php | ||
|
|
||
| namespace Whilesmart\Admin\Http\Requests; | ||
|
|
||
| use Illuminate\Foundation\Http\FormRequest; | ||
|
|
||
| class CreateOfferRequest extends FormRequest | ||
| { | ||
| public function authorize(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| /** Only the shape. What each field means is the provider's to say. */ | ||
| public function rules(): array | ||
| { | ||
| return [ | ||
| 'attributes' => ['required', 'array'], | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| <?php | ||
|
|
||
| namespace Whilesmart\Admin\Support; | ||
|
|
||
| use DateTimeInterface; | ||
|
|
||
| /** | ||
| * One discount, as the console shows it. A provider keeps whatever else its | ||
| * own storage holds. | ||
| */ | ||
| final class Offer | ||
| { | ||
| /** | ||
| * @param string $value Already formatted, because only the provider knows | ||
| * whether 20 means a percentage, pennies, or seats. | ||
| * @param DateTimeInterface|null $expiresAt Whatever date the provider holds. | ||
| * @param array<string, mixed> $meta | ||
| */ | ||
| public function __construct( | ||
| public readonly string $id, | ||
| public readonly string $code, | ||
| public readonly string $value, | ||
| public readonly ?DateTimeInterface $expiresAt = null, | ||
| public readonly ?int $redemptions = null, | ||
| public readonly ?int $maxRedemptions = null, | ||
| public readonly bool $active = true, | ||
| public readonly array $meta = [], | ||
| ) {} | ||
|
|
||
| /** @return array<string, mixed> */ | ||
| public function toArray(): array | ||
| { | ||
| return [ | ||
| 'id' => $this->id, | ||
| 'code' => $this->code, | ||
| 'value' => $this->value, | ||
| 'expires_at' => $this->expiresAt?->format(DateTimeInterface::ATOM), | ||
| 'redemptions' => $this->redemptions, | ||
| 'max_redemptions' => $this->maxRedemptions, | ||
| 'active' => $this->active, | ||
| 'meta' => $this->meta, | ||
| ]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a second transcription of the owner gate that the mail-template path already implements (
UpdateMailTemplateRequest::authorize()and the controller's template authorization both abort/deny againstOwnerAuthorizer). Two independent copies of the same rule means the offer endpoints and the template endpoints can drift — e.g. a change to owner resolution or to the 403 semantics applied to one path will not reach the other, even though both are documented as answering to the configured owner. Extract the gate into a single private helper and have the template path use the same helper, so there is exactly one place that decides who may see or change admin-owned data.