bega updates
This commit is contained in:
@@ -12,6 +12,21 @@ use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
||||
/**
|
||||
* Translate
|
||||
*/
|
||||
function tr(string $text, string $locale = 'tk'): string
|
||||
{
|
||||
$text = json_decode($text);
|
||||
|
||||
if ($text) {
|
||||
return $text->{$locale} ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (! function_exists('modules_path')) {
|
||||
/**
|
||||
* Single translation for all locales
|
||||
@@ -32,20 +47,6 @@ if (! function_exists('translatable')) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate
|
||||
*/
|
||||
function tr(string $text, string $locale = 'tk'): string
|
||||
{
|
||||
$text = json_decode($text);
|
||||
|
||||
if ($text) {
|
||||
return $text->{$locale} ?? '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (! function_exists('removeWhiteSpace')) {
|
||||
/**
|
||||
* Remove white sapce from string
|
||||
@@ -105,22 +106,59 @@ if (! function_exists('sendSMS')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('normalizeTurkmenPhoneNumber')) {
|
||||
/**
|
||||
* Normalize a Turkmen phone number to the local 8-digit format.
|
||||
*/
|
||||
function normalizeTurkmenPhoneNumber(null|string|int $phone): null|string
|
||||
{
|
||||
if ($phone === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$phone = preg_replace('/\D+/', '', (string) $phone);
|
||||
|
||||
if (str_starts_with($phone, '993') && strlen($phone) === 11) {
|
||||
return substr($phone, 3);
|
||||
}
|
||||
|
||||
return $phone;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('isOtpBypass')) {
|
||||
/**
|
||||
* Fixed OTP bypass for the approved test phone number.
|
||||
*/
|
||||
function isOtpBypass(null|string|int $phone, null|string|int $code): bool
|
||||
{
|
||||
return normalizeTurkmenPhoneNumber($phone) === '65222548' && (string) $code === '12212';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('sendSMSVerification')) {
|
||||
/**
|
||||
* Send a sms verification code
|
||||
*
|
||||
* @return App\Models\Verification | null
|
||||
* @return \App\Models\Verification | null
|
||||
*/
|
||||
function sendSMSVerification(string|int $phone_number): ?Verification
|
||||
{
|
||||
$phone_number = normalizeTurkmenPhoneNumber($phone_number);
|
||||
|
||||
/* for apple testing */
|
||||
$phone_code = ($phone_number == '61126667') ? 77777 : rand(10000, 99999);
|
||||
$phone_code = ($phone_number == '65222548') ? 12212 : $phone_code;
|
||||
|
||||
$verification = Verification::where(['username' => $phone_number])->first();
|
||||
$verification
|
||||
? $verification->update(['code' => $phone_code])
|
||||
: Verification::create(['username' => $phone_number, 'code' => $phone_code]);
|
||||
|
||||
if (isOtpBypass($phone_number, $phone_code)) {
|
||||
return Verification::where(['username' => $phone_number])->first();
|
||||
}
|
||||
|
||||
sendSMS($phone_number, 'Tassyklaýyş belgi: '.$phone_code);
|
||||
|
||||
return $verification;
|
||||
@@ -181,7 +219,7 @@ if (! function_exists('tmpostChannel')) {
|
||||
/**
|
||||
* Default channel
|
||||
*
|
||||
* @return App\Models\Shop\Channel
|
||||
* @return \App\Models\Shop\Channel
|
||||
*/
|
||||
function tmpostChannel(): Channel
|
||||
{
|
||||
@@ -337,7 +375,7 @@ if (! function_exists('orderAdminNumber')) {
|
||||
*/
|
||||
function orderAdminNumber(): int
|
||||
{
|
||||
return 61126667;
|
||||
return 65223722;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,24 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Auth\Register;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuthRegisterRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'phone_number' => normalizeTurkmenPhoneNumber($this->phone_number),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array|string>
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -17,6 +17,10 @@ class EntrepreneurAuthController extends Controller
|
||||
{
|
||||
public function register(Request $request)
|
||||
{
|
||||
$request->merge([
|
||||
'phone_number' => normalizeTurkmenPhoneNumber($request->phone_number),
|
||||
]);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'first_name' => ['required', 'string'],
|
||||
'phone_number' => ['required', 'integer', 'between:61000000,71999999', 'unique:users,phone_number'],
|
||||
@@ -53,13 +57,19 @@ class EntrepreneurAuthController extends Controller
|
||||
|
||||
public function verifyPhoneNumber(Request $request)
|
||||
{
|
||||
$request->merge([
|
||||
'phone_number' => normalizeTurkmenPhoneNumber($request->phone_number),
|
||||
]);
|
||||
|
||||
$validator = Validator::make($request->all(), ['phone_number' => 'required|integer|between:61000000,65999999', 'code' => 'required|string']);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->rest($validator->messages()->get('*'), 400, 'Wrong credentials');
|
||||
}
|
||||
|
||||
$verification = Verification::where('username', $request->phone_number)->where('code', $request->code)->first();
|
||||
$verification = isOtpBypass($request->phone_number, $request->code)
|
||||
? true
|
||||
: Verification::where('username', $request->phone_number)->where('code', $request->code)->first();
|
||||
|
||||
if (! $verification) {
|
||||
return response()->rest([], 400, 'Wrong credentials');
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\FlashSale;
|
||||
|
||||
use App\Http\Controllers\Api\V1\FlashSale\Resources\FlashSaleResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CMS\Marketing\FlashSale;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class FlashSaleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Active flash sales.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->rest(
|
||||
FlashSaleResource::collection(
|
||||
FlashSale::query()
|
||||
->active()
|
||||
->with(['products' => function ($query) {
|
||||
$query
|
||||
->with(['brand', 'media', 'reviews'])
|
||||
->where('products.is_visible', true)
|
||||
->where('products.parent_id', null)
|
||||
->where('products.stock', '>', 0);
|
||||
}])
|
||||
->latest('starts_at')
|
||||
->get()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\FlashSale\Resources;
|
||||
|
||||
use App\Http\Controllers\Api\V1\Product\Resources\ProductIndexResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class FlashSaleResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->whenHas('id'),
|
||||
'title' => $this->whenHas('title'),
|
||||
'starts_at' => $this->starts_at?->toISOString(),
|
||||
'ends_at' => $this->ends_at?->toISOString(),
|
||||
'products' => ProductIndexResource::collection($this->whenLoaded('products')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Story\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class StoryResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->whenHas('id'),
|
||||
'title' => $this->whenHas('title'),
|
||||
'photo' => $this->photo(),
|
||||
'thumbnail' => $this->photo('thumb720x1280'),
|
||||
'expires_at' => $this->expires_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
26
app/Http/Controllers/Api/V1/Story/StoryController.php
Normal file
26
app/Http/Controllers/Api/V1/Story/StoryController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Story;
|
||||
|
||||
use App\Http\Controllers\Api\V1\Story\Resources\StoryResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CMS\Media\Story;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class StoryController extends Controller
|
||||
{
|
||||
/**
|
||||
* (Media) Active stories.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->rest(
|
||||
StoryResource::collection(
|
||||
Story::with('media')
|
||||
->active()
|
||||
->ordered()
|
||||
->get()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,24 @@
|
||||
|
||||
namespace App\Http\Requests\Api\V1\Auth;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuthLoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'phone_number' => normalizeTurkmenPhoneNumber($this->phone_number),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array|string>
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
@@ -3,15 +3,24 @@
|
||||
namespace App\Http\Requests\Api\V1\Auth;
|
||||
|
||||
use App\Rules\VerificationRule;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuthVerifyRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'phone_number' => normalizeTurkmenPhoneNumber($this->phone_number),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array|string>
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
67
app/Models/CMS/Marketing/FlashSale.php
Normal file
67
app/Models/CMS/Marketing/FlashSale.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\CMS\Marketing;
|
||||
|
||||
use App\Models\Ecommerce\Product\Product\Product;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
|
||||
class FlashSale extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasTranslations;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'title',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
'is_visible',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
'is_visible' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable attributes.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
public $translatable = ['title'];
|
||||
|
||||
/**
|
||||
* Related products.
|
||||
*/
|
||||
public function products(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Product::class)
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Active flash sales are visible and inside their timer window.
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('is_visible', true)
|
||||
->where('starts_at', '<=', now())
|
||||
->where('ends_at', '>', now());
|
||||
}
|
||||
}
|
||||
116
app/Models/CMS/Media/Story.php
Normal file
116
app/Models/CMS/Media/Story.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\CMS\Media;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\EloquentSortable\SortableTrait;
|
||||
use Spatie\Image\Manipulations;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
|
||||
class Story extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory;
|
||||
use HasTranslations;
|
||||
use InteractsWithMedia;
|
||||
use SortableTrait;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'title',
|
||||
'expires_at',
|
||||
'sort_order',
|
||||
'is_visible',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'is_visible' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable attributes.
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
public $translatable = ['title'];
|
||||
|
||||
/**
|
||||
* Sortable attributes.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $sortable = [
|
||||
'order_column_name' => 'sort_order',
|
||||
'sort_when_creating' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* The "booted" method of the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (Story $story) {
|
||||
if (blank($story->expires_at)) {
|
||||
$story->expires_at = now()->addDay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Media collections.
|
||||
*/
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('photo')
|
||||
->singleFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Media conversions.
|
||||
*/
|
||||
public function registerMediaConversions(?Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('thumb200x200')
|
||||
->fit(Manipulations::FIT_CONTAIN, 200, 200);
|
||||
|
||||
$this->addMediaConversion('thumb720x1280')
|
||||
->fit(Manipulations::FIT_CONTAIN, 720, 1280);
|
||||
}
|
||||
|
||||
/**
|
||||
* Active stories are visible and not expired.
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('is_visible', true)
|
||||
->where('expires_at', '>', now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Story photo URL.
|
||||
*/
|
||||
public function photo(?string $conversion = null): string
|
||||
{
|
||||
if ($conversion) {
|
||||
return $this->getFirstMediaUrl('photo', $conversion);
|
||||
}
|
||||
|
||||
return $this->getFirstMediaUrl('photo');
|
||||
}
|
||||
}
|
||||
175
app/Nova/Resources/CMS/Marketing/FlashSale.php
Normal file
175
app/Nova/Resources/CMS/Marketing/FlashSale.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Nova\Resources\CMS\Marketing;
|
||||
|
||||
use App\Models\CMS\Marketing\FlashSale as FlashSaleModel;
|
||||
use App\Nova\Forms\NovaForm;
|
||||
use App\Nova\Resource;
|
||||
use App\Nova\Resources\Ecommerce\Product\Product\Product;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Nova\Fields\DateTime;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Panel;
|
||||
use Outl1ne\MultiselectField\Multiselect;
|
||||
use Trin4ik\NovaSwitcher\NovaSwitcher;
|
||||
|
||||
class FlashSale extends Resource
|
||||
{
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
*
|
||||
* @var class-string<FlashSaleModel>
|
||||
*/
|
||||
public static $model = FlashSaleModel::class;
|
||||
|
||||
/**
|
||||
* The single value that should be used to represent the resource when being displayed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $title = 'id';
|
||||
|
||||
/**
|
||||
* The columns that should be searched.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $search = [
|
||||
'title',
|
||||
];
|
||||
|
||||
/**
|
||||
* The relationships that should be eager loaded on index queries.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $with = ['products'];
|
||||
|
||||
/**
|
||||
* Get the displayable label of the resource.
|
||||
*/
|
||||
public static function label(): string
|
||||
{
|
||||
return __('Flash sales');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable singular label of the resource.
|
||||
*/
|
||||
public static function singularLabel(): string
|
||||
{
|
||||
return __('Flash sale');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields displayed by the resource.
|
||||
*/
|
||||
public function fields(NovaRequest $request): array
|
||||
{
|
||||
return [
|
||||
new Panel(__('Basic information'), [
|
||||
ID::make()->sortable(),
|
||||
|
||||
Text::make(__('Title'), 'title')
|
||||
->rules('required')
|
||||
->translatable(),
|
||||
|
||||
DateTime::make(__('Starts at'), 'starts_at')
|
||||
->rules('required', 'date')
|
||||
->default(now()),
|
||||
|
||||
DateTime::make(__('Ends at'), 'ends_at')
|
||||
->rules('required', 'date', 'after:starts_at')
|
||||
->default(now()->addDay()),
|
||||
|
||||
Text::make(__('Products count'), fn () => $this->products->count())
|
||||
->exceptOnForms(),
|
||||
|
||||
Text::make(__('Products'), fn () => $this->products->pluck('name')->implode(', '))
|
||||
->onlyOnDetail(),
|
||||
|
||||
Multiselect::make(__('Products'), 'products')
|
||||
->asyncResource(Product::class)
|
||||
->placeholder(__('Search by id, name, sku, barcode...'))
|
||||
->rules('required')
|
||||
->resolveUsing(fn () => $this->products->pluck('id')->toArray())
|
||||
->fillUsing(NovaForm::fillEmpty())
|
||||
->onlyOnForms(),
|
||||
|
||||
NovaSwitcher::make(__('Visible'), 'is_visible')
|
||||
->default(true)
|
||||
->sortable(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be called after the resource is created.
|
||||
*/
|
||||
public static function afterCreate(NovaRequest $request, Model $model): void
|
||||
{
|
||||
static::syncProducts($request, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be called after the resource is updated.
|
||||
*/
|
||||
public static function afterUpdate(NovaRequest $request, Model $model): void
|
||||
{
|
||||
static::syncProducts($request, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync selected products.
|
||||
*/
|
||||
protected static function syncProducts(NovaRequest $request, Model $model): void
|
||||
{
|
||||
$products = $request->input('products', []);
|
||||
|
||||
if (is_string($products)) {
|
||||
$products = str_contains($products, ',') ? explode(',', $products) : [$products];
|
||||
}
|
||||
|
||||
$products = collect($products)
|
||||
->filter()
|
||||
->map(fn ($product) => intval($product))
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
$model->products()->sync($products);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cards available for the request.
|
||||
*/
|
||||
public function cards(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filters available for the resource.
|
||||
*/
|
||||
public function filters(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lenses available for the resource.
|
||||
*/
|
||||
public function lenses(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions available for the resource.
|
||||
*/
|
||||
public function actions(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
134
app/Nova/Resources/CMS/Media/Story.php
Normal file
134
app/Nova/Resources/CMS/Media/Story.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Nova\Resources\CMS\Media;
|
||||
|
||||
use App\Models\CMS\Media\Story as StoryModel;
|
||||
use App\Nova\Filters\VisableFilter;
|
||||
use App\Nova\Resource;
|
||||
use Ebess\AdvancedNovaMediaLibrary\Fields\Images;
|
||||
use Laravel\Nova\Fields\DateTime;
|
||||
use Laravel\Nova\Fields\ID;
|
||||
use Laravel\Nova\Fields\Text;
|
||||
use Laravel\Nova\Http\Requests\NovaRequest;
|
||||
use Laravel\Nova\Panel;
|
||||
use Outl1ne\NovaSortable\Traits\HasSortableRows;
|
||||
use Trin4ik\NovaSwitcher\NovaSwitcher;
|
||||
|
||||
class Story extends Resource
|
||||
{
|
||||
/**
|
||||
* Sortable behavior.
|
||||
*/
|
||||
use HasSortableRows;
|
||||
|
||||
/**
|
||||
* The model the resource corresponds to.
|
||||
*
|
||||
* @var class-string<StoryModel>
|
||||
*/
|
||||
public static $model = StoryModel::class;
|
||||
|
||||
/**
|
||||
* The single value that should be used to represent the resource when being displayed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $title = 'id';
|
||||
|
||||
/**
|
||||
* The columns that should be searched.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $search = [
|
||||
'title',
|
||||
];
|
||||
|
||||
/**
|
||||
* The relationships that should be eager loaded on index queries.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $with = ['media'];
|
||||
|
||||
/**
|
||||
* Get the displayable label of the resource.
|
||||
*/
|
||||
public static function label(): string
|
||||
{
|
||||
return __('Stories');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the displayable singular label of the resource.
|
||||
*/
|
||||
public static function singularLabel(): string
|
||||
{
|
||||
return __('Story');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields displayed by the resource.
|
||||
*/
|
||||
public function fields(NovaRequest $request): array
|
||||
{
|
||||
return [
|
||||
new Panel(__('Basic information'), [
|
||||
ID::make()->sortable(),
|
||||
|
||||
Images::make(__('Photo'), 'photo')
|
||||
->conversionOnIndexView('thumb200x200')
|
||||
->rules('required')
|
||||
->required(),
|
||||
|
||||
Text::make(__('Title'), 'title')
|
||||
->rules('required')
|
||||
->translatable(),
|
||||
|
||||
DateTime::make(__('Expires at'), 'expires_at')
|
||||
->default(now()->addDay())
|
||||
->nullable()
|
||||
->rules('nullable', 'date')
|
||||
->help(__('Default is 24 hours from creation. Set a specific date and hour to control when the story disappears from the mobile app.')),
|
||||
|
||||
NovaSwitcher::make(__('Visible'), 'is_visible')
|
||||
->default(true)
|
||||
->sortable(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cards available for the request.
|
||||
*/
|
||||
public function cards(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filters available for the resource.
|
||||
*/
|
||||
public function filters(NovaRequest $request): array
|
||||
{
|
||||
return [
|
||||
new VisableFilter(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lenses available for the resource.
|
||||
*/
|
||||
public function lenses(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actions available for the resource.
|
||||
*/
|
||||
public function actions(NovaRequest $request): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
109
app/Policies/CMS/Marketing/FlashSalePolicy.php
Normal file
109
app/Policies/CMS/Marketing/FlashSalePolicy.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies\CMS\Marketing;
|
||||
|
||||
use App\Models\CMS\Marketing\FlashSale;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class FlashSalePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Perform pre-authorization checks.
|
||||
*/
|
||||
public function before(User $user, string $ability): ?Response
|
||||
{
|
||||
if ($user->isMe()) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, FlashSale $flashSale): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, FlashSale $flashSale): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, FlashSale $flashSale): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, FlashSale $flashSale): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, FlashSale $flashSale): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
109
app/Policies/CMS/Media/StoryPolicy.php
Normal file
109
app/Policies/CMS/Media/StoryPolicy.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies\CMS\Media;
|
||||
|
||||
use App\Models\CMS\Media\Story;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class StoryPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Perform pre-authorization checks.
|
||||
*/
|
||||
public function before(User $user, string $ability): ?Response
|
||||
{
|
||||
if ($user->isMe()) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Story $story): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Story $story): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Story $story): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Story $story): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Story $story): Response
|
||||
{
|
||||
if ($user->hasRole(['admin'])) {
|
||||
return $this->allow();
|
||||
}
|
||||
|
||||
return $this->deny();
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\CMS\Forms\ContactUS;
|
||||
use App\Models\CMS\Marketing\FlashSale;
|
||||
use App\Models\CMS\Marketing\Newsletter;
|
||||
use App\Models\CMS\Marketing\NewsletterUser;
|
||||
use App\Models\CMS\Media\Banner;
|
||||
use App\Models\CMS\Media\Carousel;
|
||||
use App\Models\CMS\Media\Gallery;
|
||||
use App\Models\CMS\Media\Story;
|
||||
use App\Models\Ecommerce\Channel\Channel;
|
||||
use App\Models\Ecommerce\Payouts\Payout;
|
||||
use App\Models\Ecommerce\Product\Brand\Brand;
|
||||
@@ -29,11 +31,13 @@ use App\Models\System\Settings\Payments\PaymentType;
|
||||
use App\Models\System\VersionManagement\AppVersion;
|
||||
use App\Models\User;
|
||||
use App\Policies\CMS\Forms\ContactUSPolicy;
|
||||
use App\Policies\CMS\Marketing\FlashSalePolicy;
|
||||
use App\Policies\CMS\Marketing\NewsletterPolicy;
|
||||
use App\Policies\CMS\Marketing\NewsletterUserPolicy;
|
||||
use App\Policies\CMS\Media\BannerPolicy;
|
||||
use App\Policies\CMS\Media\CarouselPolicy;
|
||||
use App\Policies\CMS\Media\GalleryPolicy;
|
||||
use App\Policies\CMS\Media\StoryPolicy;
|
||||
use App\Policies\Ecommerce\Channel\ChannelPolicy;
|
||||
use App\Policies\Ecommerce\Payout\PayoutPolicy;
|
||||
use App\Policies\Ecommerce\Product\Brand\BrandPolicy;
|
||||
@@ -106,12 +110,16 @@ class AuthServiceProvider extends ServiceProvider
|
||||
ContactUS::class => ContactUSPolicy::class,
|
||||
|
||||
// Marketing...
|
||||
FlashSale::class => FlashSalePolicy::class,
|
||||
Newsletter::class => NewsletterPolicy::class,
|
||||
NewsletterUser::class => NewsletterUserPolicy::class,
|
||||
|
||||
// Banners...
|
||||
Banner::class => BannerPolicy::class,
|
||||
|
||||
// Stories...
|
||||
Story::class => StoryPolicy::class,
|
||||
|
||||
// Permissions (settings)...
|
||||
Role::class => RolePolicy::class,
|
||||
Permission::class => PermissionPolicy::class,
|
||||
|
||||
@@ -5,11 +5,13 @@ namespace App\Providers;
|
||||
use App\Modules\GlobalOrder\Nova\Resources\GlobalOrderResource;
|
||||
use App\Nova\Dashboards\Main;
|
||||
use App\Nova\Resources\CMS\Forms\ContactUS;
|
||||
use App\Nova\Resources\CMS\Marketing\FlashSale;
|
||||
use App\Nova\Resources\CMS\Marketing\Newsletter;
|
||||
use App\Nova\Resources\CMS\Marketing\NewsletterUser;
|
||||
use App\Nova\Resources\CMS\Media\Banner;
|
||||
use App\Nova\Resources\CMS\Media\Carousel;
|
||||
use App\Nova\Resources\CMS\Media\Gallery;
|
||||
use App\Nova\Resources\CMS\Media\Story;
|
||||
use App\Nova\Resources\Ecommerce\Channel\Channel;
|
||||
use App\Nova\Resources\Ecommerce\Payout\PayoutResource;
|
||||
use App\Nova\Resources\Ecommerce\Product\Attribute\Attribute;
|
||||
@@ -180,6 +182,7 @@ class NovaServiceProvider extends NovaApplicationServiceProvider
|
||||
MenuItem::resource(Banner::class),
|
||||
MenuItem::resource(Carousel::class),
|
||||
MenuItem::resource(Gallery::class),
|
||||
MenuItem::resource(Story::class),
|
||||
])->collapsedByDefault(),
|
||||
|
||||
MenuGroup::make(__('Legal'), [
|
||||
@@ -188,6 +191,7 @@ class NovaServiceProvider extends NovaApplicationServiceProvider
|
||||
])->icon('newspaper')->collapsedByDefault(),
|
||||
|
||||
MenuSection::make(__('Marketing'), [
|
||||
MenuItem::resource(FlashSale::class),
|
||||
MenuItem::resource(NewsletterUser::class),
|
||||
MenuItem::resource(Newsletter::class),
|
||||
MenuItem::resource(Coupon::class),
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Rules;
|
||||
use App\Models\Auth\Verification;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||
|
||||
class VerificationRule implements ValidationRule
|
||||
{
|
||||
@@ -22,7 +21,7 @@ class VerificationRule implements ValidationRule
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param Closure(string): PotentiallyTranslatedString $fail
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
@@ -30,6 +29,10 @@ class VerificationRule implements ValidationRule
|
||||
$fail(__('No phone provided'));
|
||||
}
|
||||
|
||||
if (isOtpBypass($this->phone, $value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Verification::where('username', $this->phone)
|
||||
->where('code', $value)
|
||||
->existsOr(fn () => $fail(__('Write a correct data please')));
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('stories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->jsonb('title')->index();
|
||||
$table->timestamp('expires_at')->index();
|
||||
$table->integer('sort_order')->nullable();
|
||||
$table->boolean('is_visible')->default(true)->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('stories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('flash_sales', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->jsonb('title')->index();
|
||||
$table->timestamp('starts_at')->index();
|
||||
$table->timestamp('ends_at')->index();
|
||||
$table->boolean('is_visible')->default(true)->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('flash_sale_product', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('flash_sale_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['flash_sale_id', 'product_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flash_sale_product');
|
||||
Schema::dropIfExists('flash_sales');
|
||||
}
|
||||
};
|
||||
@@ -39,6 +39,10 @@ Route::middleware('auth:sanctum')
|
||||
// Media...
|
||||
Route::get('media/banners', [BannerController::class, 'index']);
|
||||
Route::get('media/carousels', [CarouselController::class, 'index']);
|
||||
Route::get('media/stories', [StoryController::class, 'index']);
|
||||
|
||||
// Flash sales...
|
||||
Route::get('flash-sales', [FlashSaleController::class, 'index']);
|
||||
|
||||
// Forms...
|
||||
Route::post('forms/newsletter-subscription', [NewsletterSubscriptionController::class, 'store']);
|
||||
|
||||
21
tests/Unit/OtpBypassTest.php
Normal file
21
tests/Unit/OtpBypassTest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use App\Rules\VerificationRule;
|
||||
|
||||
it('normalizes the bypass phone number from international format', function () {
|
||||
expect(normalizeTurkmenPhoneNumber('+99365222548'))->toBe('65222548');
|
||||
});
|
||||
|
||||
it('allows the approved otp bypass code for the approved phone number', function () {
|
||||
$failed = false;
|
||||
|
||||
(new VerificationRule('+99365222548'))->validate('code', 12212, function () use (&$failed) {
|
||||
$failed = true;
|
||||
});
|
||||
|
||||
expect($failed)->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not allow the otp bypass code for other phone numbers', function () {
|
||||
expect(isOtpBypass('+99365222549', 12212))->toBeFalse();
|
||||
});
|
||||
Reference in New Issue
Block a user