Revision 827

Date:
2021/09/08 15:14:01
Author:
ahitrov
Revision Log:
CDEK service

Files:

Legend:

 
Added
 
Removed
 
Modified
  • utf8/plugins/webshop/lib/webshop/Service/Base.pm

     
    1 package webshop::Service::Base;
    2
    3 use strict;
    4 use warnings;
    5
    6 use Digest::MD5;
    7 use Data::Dumper;
    8 use MIME::Base64;
    9 use JSON::XS;
    10 use JSON::XS::Boolean;
    11
    12 use parent 'Contenido::Accessor';
    13 __PACKAGE__->mk_accessors(qw(token key api_url error));
    14
    15 use Contenido::Globals;
    16
    17 sub new {
    18 my ($proto, $args) = @_;
    19 my $class = ref($proto) || $proto;
    20 $args //= {};
    21 my $self = {};
    22 bless $self, $class;
    23
    24 if ( ref $args ne 'HASH' ) {
    25 $self->error("INIT: argument list is empty");
    26 return $self;
    27 }
    28
    29 if ( exists $args->{api_url} && $args->{api_url} ) {
    30 $self->api_url($args->{api_url});
    31 } else {
    32 $self->error("INIT: API url is empty");
    33 return $self;
    34 }
    35
    36 if ( exists $args->{token} && $args->{token} ) {
    37 $self->token($args->{token});
    38 } else {
    39 $self->error("INIT: API token is empty");
    40 return $self;
    41 }
    42
    43 if ( exists $args->{key} && $args->{key} ) {
    44 $self->key($args->{auth_key});
    45 } else {
    46 $self->error("INIT: API auth key is empty");
    47 return $self;
    48 }
    49
    50 warn Dumper $self if $DEBUG;
    51 return $self;
    52 }
    53
    54
    55 sub _MakeRequest {
    56 my ($self, $url, $type, $body, $cache_control) = @_;
    57
    58 $type //= 'get';
    59
    60 my ($key, $file, $exists, $expired);
    61 my $time = time();
    62 my $cache_container = $state->{data_dir}. '/webshop_service_cache';
    63 if ( ref $cache_control ) {
    64 unless ( -e $cache_container ) {
    65 `mkdir -p $cache_container`;
    66 }
    67 $key = 'service_api_'.Digest::MD5::md5_hex( $url );
    68 $file = $cache_container . '/' . $key;
    69 if ( -e $file ) {
    70 $exists = 1;
    71 my $created = (stat $file)[9];
    72 if ( exists $cache_control->{expires_at} && $cache_control->{expires_at} > 0 ) {
    73 if ( $time - $created > $cache_control->{expires_at} ) {
    74 $expired = 1;
    75 }
    76 } elsif ( exists $cache_control->{expires_at} && $cache_control->{expires_at} < 0 ) {
    77 $expired = 0;
    78 } else {
    79 $expired = 1;
    80 }
    81 } else {
    82 $expired = 1;
    83 }
    84 if ( $exists && !$expired ) {
    85 my $content = do $file;
    86 return $content;
    87 }
    88 }
    89
    90
    91 my $ua = LWP::UserAgent->new;
    92 $ua->timeout( 10 );
    93 $ua->agent('Mozilla/5.0');
    94
    95 if ( $self->token ) {
    96 warn "Auth by token: ".$self->token."\n" if $DEBUG;
    97 $ua->default_header( 'Authorization' => "AccessToken ".$self->token );
    98 }
    99 if ( $self->key ) {
    100 warn "Auth by auth key: ".$self->key."\n" if $DEBUG;
    101 $ua->default_header( 'X-User-Authorization' => "Basic ".$self->key );
    102 }
    103 $ua->default_header( 'Content-Type' => 'application/json' );
    104 $ua->default_header( 'Accept' => 'application/json;charset=UTF-8' );
    105
    106 if ( ref $body ) {
    107 $body = encode_json( $body );
    108 }
    109
    110 my $uri;
    111 if ( $url =~ /^http/ ) {
    112 $uri = URI->new( $url );
    113 } else {
    114 $uri = URI->new( $self->api_url.($url =~ /^\// ? '' : '/').$url );
    115 }
    116
    117 my $res;
    118 if ( $type eq 'post' ) {
    119 my $req = HTTP::Request->new(POST => $uri);
    120 $req->content_type('application/json');
    121 $req->content($body);
    122 # warn "RUPOST post JSON: $body\n" if $DEBUG;
    123 $res = $ua->request($req);
    124 } elsif ( $type eq 'delete' ) {
    125 $res = $ua->delete( $uri );
    126 } else {
    127 $res = $ua->get( $uri );
    128 }
    129
    130 warn Dumper($res) if $DEBUG && $res->code != 200;
    131 if ( $res->code != 200 && $exists ) {
    132 my $content = do $file;
    133 return $content;
    134 }
    135
    136 my $result = {
    137 code => $res->code,
    138 status => $res->status_line,
    139 content => JSON::XS->new->utf8->decode( $res->decoded_content ),
    140 };
    141
    142 if ( $res->code == 200 && ref $cache_control ) {
    143 my $fh;
    144 if ( open($fh, '>', $file) ) {
    145 local $Data::Dumper::Indent = 0;
    146 print $fh Dumper( $result );
    147 close $fh;
    148 }
    149 }
    150
    151 return $result;
    152 }
    153
    154 1;
  • utf8/plugins/webshop/lib/webshop/Service/CDEK.pm

     
    1 package webshop::Service::CDEK;
    2
    3 use strict;
    4 use warnings 'all';
    5
    6 use base 'webshop::Service::Base';
    7 use Contenido::Globals;
    8 use payments::Keeper;
    9 use URI::Escape;
    10 use Types::Serialiser;
    11 use Digest::MD5;
    12 use Data::Dumper;
    13 use utf8;
    14
    15 use constant {
    16 COURIER_TARIFF_PRIORITY => 'courier',
    17 PICKUP_TARIFF_PRIORITY => 'pickup',
    18 };
    19
    20 our $translate = {
    21 'rus' => {
    22 'YOURCITY' => 'Ваш город',
    23 'COURIER' => 'Курьер',
    24 'PICKUP' => 'Самовывоз',
    25 'TERM' => 'Срок',
    26 'PRICE' => 'Стоимость',
    27 'DAY' => 'дн.',
    28 'RUB' => ' руб.',
    29 'KZT' => 'KZT',
    30 'USD' => 'USD',
    31 'EUR' => 'EUR',
    32 'GBP' => 'GBP',
    33 'CNY' => 'CNY',
    34 'BYN' => 'BYN',
    35 'UAH' => 'UAH',
    36 'KGS' => 'KGS',
    37 'AMD' => 'AMD',
    38 'TRY' => 'TRY',
    39 'THB' => 'THB',
    40 'KRW' => 'KRW',
    41 'AED' => 'AED',
    42 'UZS' => 'UZS',
    43 'MNT' => 'MNT',
    44 'NODELIV' => 'Нет доставки',
    45 'CITYSEARCH' => 'Поиск города',
    46 'ALL' => 'Все',
    47 'PVZ' => 'Пункты выдачи',
    48 'POSTOMAT' => 'Постаматы',
    49 'MOSCOW' => 'Москва',
    50 'RUSSIA' => 'Россия',
    51 'COUNTING' => 'Идет расчет',
    52
    53 'NO_AVAIL' => 'Нет доступных способов доставки',
    54 'CHOOSE_TYPE_AVAIL' => 'Выберите способ доставки',
    55 'CHOOSE_OTHER_CITY' => 'Выберите другой населенный пункт',
    56
    57 'TYPE_ADDRESS' => 'Уточните адрес',
    58 'TYPE_ADDRESS_HERE' => 'Введите адрес доставки',
    59
    60 'L_ADDRESS' => 'Адрес пункта выдачи заказов',
    61 'L_TIME' => 'Время работы',
    62 'L_WAY' => 'Как к нам проехать',
    63 'L_CHOOSE' => 'Выбрать',
    64
    65 'H_LIST' => 'Список пунктов выдачи заказов',
    66 'H_PROFILE' => 'Способ доставки',
    67 'H_CASH' => 'Расчет картой',
    68 'H_DRESS' => 'С примеркой',
    69 'H_POSTAMAT' => 'Постаматы СДЭК',
    70 'H_SUPPORT' => 'Служба поддержки',
    71 'H_QUESTIONS' => 'Если у вас есть вопросы, можете<br> задать их нашим специалистам',
    72 'ADDRESS_WRONG' => 'Невозможно определить выбранное местоположение. Уточните адрес из выпадающего списка в адресной строке.',
    73 'ADDRESS_ANOTHER' => 'Ознакомьтесь с новыми условиями доставки для выбранного местоположения.'
    74 },
    75 'eng' => {
    76 'YOURCITY' => 'Your city',
    77 'COURIER' => 'Courier',
    78 'PICKUP' => 'Pickup',
    79 'TERM' => 'Term',
    80 'PRICE' => 'Price',
    81 'DAY' => 'days',
    82 'RUB' => 'RUB',
    83 'KZT' => 'KZT',
    84 'USD' => 'USD',
    85 'EUR' => 'EUR',
    86 'GBP' => 'GBP',
    87 'CNY' => 'CNY',
    88 'BYN' => 'BYN',
    89 'UAH' => 'UAH',
    90 'KGS' => 'KGS',
    91 'AMD' => 'AMD',
    92 'TRY' => 'TRY',
    93 'THB' => 'THB',
    94 'KRW' => 'KRW',
    95 'AED' => 'AED',
    96 'UZS' => 'UZS',
    97 'MNT' => 'MNT',
    98 'NODELIV' => 'Not delivery',
    99 'CITYSEARCH' => 'Search for a city',
    100 'ALL' => 'All',
    101 'PVZ' => 'Points of self-delivery',
    102 'POSTOMAT' => 'Postamats',
    103 'MOSCOW' => 'Moscow',
    104 'RUSSIA' => 'Russia',
    105 'COUNTING' => 'Calculation',
    106
    107 'NO_AVAIL' => 'No shipping methods available',
    108 'CHOOSE_TYPE_AVAIL' => 'Choose a shipping method',
    109 'CHOOSE_OTHER_CITY' => 'Choose another location',
    110
    111 'TYPE_ADDRESS' => 'Specify the address',
    112 'TYPE_ADDRESS_HERE' => 'Enter the delivery address',
    113
    114 'L_ADDRESS' => 'Adress of self-delivery',
    115 'L_TIME' => 'Working hours',
    116 'L_WAY' => 'How to get to us',
    117 'L_CHOOSE' => 'Choose',
    118
    119 'H_LIST' => 'List of self-delivery',
    120 'H_PROFILE' => 'Shipping method',
    121 'H_CASH' => 'Payment by card',
    122 'H_DRESS' => 'Dressing room',
    123 'H_POSTAMAT' => 'Postamats CDEK',
    124 'H_SUPPORT' => 'Support',
    125 'H_QUESTIONS' => 'If you have any questions,<br> you can ask them to our specialists',
    126
    127 'ADDRESS_WRONG' => 'Impossible to define address. Please, recheck the address.',
    128 'ADDRESS_ANOTHER' => 'Read the new terms and conditions.'
    129 },
    130 };
    131
    132
    133 sub new {
    134 my ($proto, %params) = @_;
    135 my $class = ref($proto) || $proto;
    136 my $m = shift;
    137
    138 my $self = {};
    139 bless $self, $class;
    140
    141 my $obj = $self->SUPER::new( { api_url => 'http://api.cdek.ru' } );
    142 $obj->{m} = $m;
    143
    144 $obj->{courierTariffPriority} = $state->{webshop}->cdek_courier_tariff_priority;
    145 $obj->{pickupTariffPriority} = $state->{webshop}->cdek_pickup_tariff_priority;
    146 $obj->{account} = $state->{webshop}->cdek_account;
    147 $obj->{key} = $state->{webshop}->cdek_key;
    148
    149 return $obj;
    150 }
    151
    152
    153 #############################################################################################
    154 ### Actions
    155 #############################################################################################
    156 sub getPVZ {
    157 my $self = shift;
    158 my $args = shift || {};
    159 my $langPart = $args->{'lang'} && exists $translate->{$args->{'lang'}} ? '&lang=' . $args->{'lang'} : '';
    160 my $cacheKey = 'cdek_getPVZ_' . $args->{'lang'} . '_cnt_' . $args->{country};
    161 if ( $keeper->MEMD && !$args->{no_cache} ) {
    162 my $cached = $keeper->MEMD->get($cacheKey);
    163 if ( $cached ) {
    164 return { 'pvz' => $cached };
    165 }
    166 }
    167 my $result = $args->{raw_data} ? $args->{raw_data} : $self->_MakeRequest( 'https://integration.cdek.ru/pvzlist/v1/json?type=ALL' . $langPart, 'get', undef, { expires_at => 30 * 3600 } );
    168 my $out = {
    169 'PVZ' => {},
    170 'CITY' => {},
    171 'REGIONS' => {},
    172 'CITYFULL' => {},
    173 'COUNTRIES' => {},
    174 };
    175 my $countryRequested = Encode::decode('utf-8', $args->{country});
    176 if ( $result->{code} == 200 && exists $result->{content}{pvz} && ref $result->{content}{pvz} eq 'ARRAY' ) {
    177 # warn "getPVZ pvz found: ".scalar(@{$result->{content}{pvz}})."\n";
    178 foreach my $val ( @{$result->{content}{pvz}} ) {
    179 if ( $countryRequested ne 'all' && $countryRequested ne $val->{countryName} ) {
    180 next;
    181 }
    182 my $cityCode = $val->{cityCode};
    183 my $type = 'PVZ';
    184 my $city = $val->{city};
    185 if ( my $pos = index($city, '(') >= 0 ) {
    186 $city = trim(substr($city, 0, $pos));
    187 }
    188 if ( my $pos = index($city, ',') >= 0 ) {
    189 $city = trim(substr($city, 0, $pos));
    190 }
    191 my $code = $val->{code};
    192 $out->{$type}{$cityCode}{$code} = {
    193 'Name' => $val->{'name'},
    194 'WorkTime' => $val->{'workTime'},
    195 'Address' => $val->{'address'},
    196 'Phone' => $val->{'phone'},
    197 'Note' => $val->{'note'},
    198 'cX' => $val->{'coordX'},
    199 'cY' => $val->{'coordY'},
    200 'Dressing' => Types::Serialiser::as_bool($val->{'isDressingRoom'}),
    201 'Cash' => Types::Serialiser::as_bool($val->{'haveCashless'}),
    202 'Postamat' => Types::Serialiser::as_bool(lc($val->{'type'}) eq 'postamat'),
    203 'Station' => $val->{'nearestStation'},
    204 'Site' => $val->{'site'},
    205 'Metro' => $val->{'metroStation'},
    206 'AddressComment' => $val->{'addressComment'},
    207 'CityCode' => $val->{'CityCode'},
    208 };
    209 if ( exists $val->{weightLimit} ) {
    210 $out->{$type}{$cityCode}{$code}{'WeightLim'} = {
    211 'MIN' => $val->{weightLimit}{'weightMin'} * 1.0,
    212 'MAX' => $val->{weightLimit}{'weightMax'} * 1.0,
    213 };
    214 }
    215 my @arImgs;
    216 if ( exists $val->{officeImageList} && ref $val->{officeImageList} eq 'ARRAY' ) {
    217 foreach my $img ( @{$val->{officeImageList}} ) {
    218 unless ( $img->{'url'} =~ /^http/ ) {
    219 next;
    220 }
    221 push @arImgs, $img->{'url'};
    222 }
    223 }
    224
    225 if ( @arImgs ) {
    226 $out->{$type}{$cityCode}{$code}{'Picture'} = \@arImgs;
    227 }
    228 if ( exists $val->{officeHowGo} ) {
    229 $out->{$type}{$cityCode}{$code}{'Path'} = $val->{officeHowGo}{'url'};
    230 }
    231
    232 if (!exists $out->{'CITY'}{$cityCode} ) {
    233 $out->{'CITY'}{$cityCode} = $city;
    234 $out->{'CITYREG'}{$cityCode} = int($val->{'regionCode'});
    235 push @{ $out->{'REGIONSMAP'}{int($val->{'regionCode'})} }, int($cityCode);
    236 $out->{'CITYFULL'}{$cityCode} = $val->{'countryName'} . ' ' . $val->{'regionName'} . ' ' . $city;
    237 $out->{'REGIONS'}{$cityCode} = join(', ', $val->{'regionName'}, $val->{'countryName'} );
    238 }
    239 }
    240 if ( $keeper->MEMD ) {
    241 $keeper->MEMD->set( $cacheKey, $out, 108000 );
    242 }
    243 return { 'pvz' => $out };
    244 }
    245 return { 'error' => 'Some error PVZ' };
    246 }
    247
    248 sub getLang {
    249 my $self = shift;
    250 my $args = shift || {};
    251 my $lang = $args->{'lang'} && exists $translate->{$args->{'lang'}} ? $args->{'lang'} : '';
    252
    253 return { 'LANG' => $self->getValue( $translate, $lang, $translate->{'rus'} ) };
    254 }
    255
    256
    257 sub getCity {
    258 my $self = shift;
    259 my $args = shift || {};
    260
    261 warn Dumper $args;
    262 my $city = $self->getValue($args, 'city');
    263 warn "Let's find for $city\n";
    264 if ( $city ) {
    265 return $self->getCityByName( $city );
    266 }
    267
    268 if ( my $address = $self->getValue($args, 'address') ) {
    269 return $self->getCityByAddress(Encode::decode('utf-8', $address));
    270 }
    271
    272 return { 'error' => 'No city to search given' };
    273 }
    274
    275 sub calc {
    276 my $self = shift;
    277 my $args = shift || {};
    278
    279 my $shipment = $self->getRequestValue($args, 'shipment', {});
    280 warn Dumper($shipment);
    281
    282 if ( !exists $shipment->{'tariffList'} ) {
    283 $shipment->{'tariffList'} = $self->getTariffPriority( $shipment->{'type'} );
    284 }
    285
    286 if ( $args->{referer} ) {
    287 $shipment->{'ref'} = $args->{referer};
    288 }
    289
    290 if ( !exists $shipment->{'cityToId'} ) {
    291 my $cityTo = $self->getCityByName( $shipment->{'cityTo'} );
    292 if ( !exists $cityTo->{error} && exists $cityTo->{id} ) {
    293 $shipment->{'cityToId'} = $cityTo->{id};
    294 }
    295 }
    296 warn Dumper($shipment) if $DEBUG;
    297
    298 if ( $shipment->{'cityToId'} ) {
    299 my $answer = $self->calculate($shipment);
    300
    301 if ( $answer ) {
    302 my $returnData = {
    303 'result' => $answer,
    304 'type' => $shipment->{'type'},
    305 };
    306 if ( $shipment->{'timestamp'} ) {
    307 $returnData->{'timestamp'} = $shipment->{'timestamp'};
    308 }
    309
    310 return $returnData;
    311 }
    312 }
    313
    314
    315 return { 'error' => 'City to not found' };
    316 }
    317 ### /Actions
    318
    319 #############################################################################################
    320 ### Helpers
    321 #############################################################################################
    322 sub calculate {
    323 my $self = shift;
    324 my $shipment = shift || {};
    325
    326 if ( !exists $shipment->{'goods'} || scalar @{$shipment->{'goods'}} == 0 ) {
    327 return { 'error' => 'The dimensions of the goods are not defined' };
    328 }
    329
    330 my $headers = $self->getHeaders();
    331
    332 my $arData = {
    333 'dateExecute' => $self->getValue($headers, 'date'),
    334 'version' => '1.0',
    335 'authLogin' => $self->getValue($headers, 'account'),
    336 'secure' => $self->getValue($headers, 'secure'),
    337 'senderCityId' => $self->getValue( $shipment, 'cityFromId' ),
    338 'receiverCityId' => $self->getValue($shipment, 'cityToId'),
    339 'ref' => $self->getValue($shipment, 'ref'),
    340 'widget' => 1,
    341 'currency' => $self->getValue($shipment, 'currency', 'RUB'),
    342 };
    343
    344 if ( exists $shipment->{'tariffList'} ) {
    345 my $priority = 1;
    346 foreach my $tariffId ( @{$shipment->{'tariffList'}} ) {
    347 $tariffId = int($tariffId);
    348 push @{$arData->{'tariffList'}}, {
    349 'priority' => $priority++,
    350 'id' => $tariffId,
    351 };
    352 }
    353 }
    354
    355 $arData->{'goods'} = [];
    356 foreach my $arGood ( @{$shipment->{'goods'}} ) {
    357 push @{$arData->{'goods'}}, {
    358 'weight' => $arGood->{'weight'},
    359 'length' => $arGood->{'length'},
    360 'width' => $arGood->{'width'},
    361 'height' => $arGood->{'height'},
    362 };
    363 }
    364
    365 my $type = $self->getValue($shipment, 'type');
    366
    367 my $resultTariffs = $self->_MakeRequest(
    368 '/calculator/calculate_tarifflist.php',
    369 'post',
    370 $arData,
    371 );
    372 if ( $resultTariffs && $resultTariffs->{'code'} == 200 ) {
    373 if ( ref $resultTariffs->{'content'} && exists $resultTariffs->{'content'}{'result'} ) {
    374 my @resultTariffs = grep { $_->{status} } @{$resultTariffs->{'content'}{'result'}};
    375 my %resultTariffs;
    376 foreach my $t ( @resultTariffs ) {
    377 $resultTariffs{$t->{'tariffId'}} = $t->{result};
    378 }
    379 warn Dumper \@resultTariffs;
    380
    381 if ( $type && !(exists $arData->{'tariffId'} && $arData->{'tariffId'}) ) {
    382 my $tariffListSorted = $self->getTariffPriority($type);
    383 foreach my $id ( @$tariffListSorted ) {
    384 if ( exists $resultTariffs{$id} ) {
    385 return $resultTariffs{$id};
    386 }
    387 }
    388 }
    389 return shift @resultTariffs;
    390 }
    391
    392 return { 'error' => 'Wrong server answer' };
    393 }
    394
    395 return { 'error' => 'Wrong answer code from server : ' . $resultTariffs->{'status'} };
    396 }
    397
    398 sub getTariffPriority {
    399 my $self = shift;
    400 my $type = shift // &COURIER_TARIFF_PRIORITY;
    401
    402 if ( $type ne &COURIER_TARIFF_PRIORITY && $type ne &PICKUP_TARIFF_PRIORITY ) {
    403 warn "Unknown tariff type $type\n";
    404 }
    405
    406 return $type eq &COURIER_TARIFF_PRIORITY ? $self->{courierTariffPriority} : $self->{pickupTariffPriority};
    407 }
    408
    409 sub getCityByName {
    410 my $self = shift;
    411 my $name = shift;
    412 my $single = shift // 1;
    413
    414 my $result = $self->_MakeRequest( '/city/getListByTerm/json.php?q=' . uri_escape($name) );
    415 if ( $result->{code} == 200 ) {
    416 unless ( $result->{content}{geonames} ) {
    417 return (undef, 'No cities found');
    418 } else {
    419 if ($single) {
    420 return {
    421 'id' => $result->{content}->{geonames}->[0]->{id},
    422 'city' => $result->{content}->{geonames}->[0]->{cityName},
    423 'region' => $result->{content}->{geonames}->[0]->{regionName},
    424 'country' => $result->{content}->{geonames}->[0]->{countryName}
    425 };
    426 } else {
    427 my $arReturn = {'cities' => []};
    428 foreach my $city ( @{$result->{content}{geonames}} ) {
    429 push @{$arReturn->{'cities'}}, {
    430 'id' => $city->{id},
    431 'city' => $city->{cityName},
    432 'region' => $city->{regionName},
    433 'country' => $city->{countryName}
    434 };
    435 }
    436 return $arReturn;
    437 }
    438 }
    439 } else {
    440 return { 'error' => 'Wrong answer code from server : ' . $result->{'code'} };
    441 }
    442 }
    443
    444 sub getCityByAddress {
    445 my $self = shift;
    446 my $address = shift;
    447
    448 my $arReturn = {};
    449 my $arStages = { 'country' => undef, 'region' => undef, 'subregion' => undef };
    450 my @arAddress = split /,/, $address;
    451
    452 my $ind = 0;
    453 ### finging country in address
    454 if ( grep { Encode::decode('utf-8', $arAddress[0]) =~ /$_/i } $self->getCountries() ) {
    455 my $country = lc(Encode::decode('utf-8', $arAddress[0]));
    456 for ($country) {
    457 s/^\s+//;
    458 s/\s+$//;
    459 }
    460 $arStages->{'country'} = Encode::encode('utf-8', $country);
    461 $ind++;
    462 }
    463
    464 ### finding region in address
    465 foreach my $regionStr ( $self->getRegion() ) {
    466 my $search = lc(Encode::decode('utf-8', $arAddress[$ind]));
    467 for ($search) {
    468 s/^\s+//;
    469 s/\s+$//;
    470 }
    471 my $indSearch = index($search, $regionStr);
    472 if ( $indSearch >= 0 ) {
    473 if ($indSearch) {
    474 $arStages->{'region'} = substr($search, 0, $indSearch);
    475 } else {
    476 $arStages->{'region'} = substr($search, length($regionStr));
    477 }
    478 for ( $arStages->{'region'} ) {
    479 s/^\s+//;
    480 s/\s+$//;
    481 }
    482 $ind++;
    483 last;
    484 }
    485 }
    486
    487 ### finding subregions
    488 foreach my $subRegionStr ( $self->getSubRegion() ) {
    489 my $search = lc(Encode::decode('utf-8', $arAddress[$ind]));
    490 my $indSearch = index($search, $subRegionStr);
    491 if ( $indSearch >= 0 ) {
    492 if ($indSearch) {
    493 $arStages->{'subregion'} = substr($search, 0, $indSearch);
    494 } else {
    495 $arStages->{'subregion'} = substr($search, length($subRegionStr));
    496 }
    497 for ( $arStages->{'subregion'} ) {
    498 s/^\s+//;
    499 s/\s+$//;
    500 }
    501 $ind++;
    502 last;
    503 }
    504 }
    505
    506 ### finding city
    507 my $cityName = Encode::decode('utf-8', $arAddress[$ind]);
    508 for ($cityName) {
    509 s/^\s+//;
    510 s/\s+$//;
    511 }
    512 my $cdekCity = $self->getCityByName(Encode::encode('utf-8', $cityName), 0);
    513
    514 if ( exists $cdekCity->{'error'} && $cdekCity->{'error'} ) {
    515 foreach my $placeLbl ( $self->getCityDef() ) {
    516 my $search = lc(Encode::decode('utf-8', $arAddress[$ind]));
    517 for ( $search ) {
    518 s/^\s+//;
    519 s/\s+$//;
    520 s/ё/е/sgi;
    521 }
    522 my $indSearch = index($search, $placeLbl);
    523 if ( $indSearch >= 0 ) {
    524 if ($indSearch) {
    525 $search = substr($search, 0, $indSearch);
    526 } else {
    527 $search = substr($search, length($placeLbl));
    528 }
    529 for ( $search ) {
    530 s/^\s+//;
    531 s/\s+$//;
    532 }
    533 $cityName = $search;
    534 $cdekCity = $self->getCityByName(Encode::encode('utf-8', $search), 0);
    535 last;
    536 }
    537 }
    538 }
    539
    540 my $pretend;
    541 if ( $cdekCity->{'error'} ) {
    542 $arReturn->{'error'} = $cdekCity->{'error'};
    543 } else {
    544 if ( ref $cdekCity->{'cities'} eq 'ARRAY' && scalar(@{$cdekCity->{'cities'}}) > 0) {
    545 my $arPretend = [];
    546
    547 ## parseCountry
    548 if ( $arStages->{'country'} ) {
    549 foreach my $arCity ( @{$cdekCity->{'cities'}} ) {
    550 my $possCountry = lc( Encode::decode('utf-8', $arCity->{'country'}) );
    551 if ( !$possCountry || index( Encode::decode('utf-8', $arStages->{'country'}), $possCountry) >= 0 ) {
    552 push @$arPretend, $arCity;
    553 }
    554 }
    555 } else {
    556 $arPretend = $cdekCity->{'cities'};
    557 }
    558
    559 ## parseRegion
    560 if ( $arStages->{'region'} && scalar @$arPretend > 1 ) {
    561 my $_arPretend = [];
    562 foreach my $arCity ( @$arPretend ) {
    563 my $possRegion = lc( Encode::decode('utf-8', $arCity->{'region'}) );
    564 my $arStagesRegion = lc( Encode::decode('utf-8', $arStages->{'region'}) );
    565 foreach my $regpart ( $self->getRegion() ) {
    566 $possRegion =~ s/$regpart//i;
    567 $arStagesRegion =~ s/$regpart//i;
    568 }
    569 for ( $possRegion, $arStagesRegion ) {
    570 s/^\s+//;
    571 s/\s+$//;
    572 }
    573 if ( !$possRegion || index($possRegion, $arStagesRegion) >= 0 ) {
    574 push @$_arPretend, $arCity;
    575 }
    576 }
    577 $arPretend = $_arPretend;
    578 }
    579
    580 ## parseSubRegion
    581 if ( $arStages->{'subregion'} && scalar @$arPretend > 1 ) {
    582 my $_arPretend = [];
    583 foreach my $arCity ( @$arPretend ) {
    584 my $possSubRegion = lc( Encode::decode('utf-8', $arCity->{'city'}) );
    585 if ( !$possSubRegion || index($possSubRegion, lc( Encode::decode('utf-8', $arStages->{'subregion'}) ) ) >= 0 ) {
    586 push @$_arPretend, $arCity;
    587 }
    588 }
    589 $arPretend = $_arPretend;
    590 }
    591
    592 ## parseUndefined
    593 ## not full city name
    594 if ( scalar @$arPretend > 1 ) {
    595 my $_arPretend = [];
    596 foreach my $arCity ( @$arPretend ) {
    597 if ( index($arCity->{'city'}, ',') >= 0 ) {
    598 push @$_arPretend, $arCity;
    599 }
    600 }
    601 $arPretend = $_arPretend;
    602 }
    603
    604 if ( scalar @$arPretend > 1) {
    605 my $_arPretend = [];
    606 foreach my $arCity ( @$arPretend ) {
    607 if ( length(Encode::decode('utf-8', $arCity->{'city'})) == length($cityName)) {
    608 push @$_arPretend, $arCity;
    609 }
    610 }
    611 $arPretend = $_arPretend;
    612 }
    613
    614 ## federalCities
    615 if ( scalar @$arPretend > 1 ) {
    616 my $_arPretend = [];
    617 foreach my $arCity ( @$arPretend ) {
    618 if ( $arCity->{'city'} eq $arCity->{'region'} ) {
    619 push @$_arPretend, $arCity;
    620 }
    621 }
    622 $arPretend = $_arPretend;
    623 }
    624
    625
    626 ## end
    627 if ( scalar @$arPretend ) {
    628 $pretend = pop @$arPretend;
    629 }
    630 } else {
    631 $pretend = $cdekCity->{'cities'}->[0];
    632 }
    633 if ( $pretend ) {
    634 $arReturn->{'city'} = $pretend;
    635 } else {
    636 $arReturn->{'error'} = 'Undefined city';
    637 }
    638 }
    639 return $arReturn;
    640 }
    641
    642 sub getCountries {
    643 return ('Россия', 'Беларусь', 'Армения', 'Казахстан', 'Киргизия', 'Молдова', 'Таджикистан', 'Узбекистан');
    644 }
    645
    646 sub getRegion {
    647 return ('автономная область', 'область', 'республика', 'автономный округ', 'округ', 'край', 'обл.');
    648 }
    649
    650 sub getSubRegion {
    651 return ('муниципальный район', 'район', 'городской округ');
    652 }
    653
    654 sub getCityDef {
    655 return(
    656 'поселок городского типа',
    657 'населенный пункт',
    658 'курортный поселок',
    659 'дачный поселок',
    660 'рабочий поселок',
    661 'почтовое отделение',
    662 'сельское поселение',
    663 'ж/д станция',
    664 'станция',
    665 'городок',
    666 'деревня',
    667 'микрорайон',
    668 'станица',
    669 'хутор',
    670 'аул',
    671 'поселок',
    672 'село',
    673 'снт'
    674 );
    675 }
    676
    677 sub getValue {
    678 my ($self, $args, $key, $default) = @_;
    679 $args //= {};
    680
    681 return (exists $args->{$key} && $args->{$key} ? $args->{$key} : $default);
    682 }
    683
    684 sub getHeaders {
    685 my $self = shift;
    686
    687 my $date = Contenido::DateTime->new()->ymd('-');
    688 my $headers = {
    689 'date' => $date,
    690 };
    691 if ( $self->{account} && $self->{key} ) {
    692 $headers = {
    693 'date' => $date,
    694 'account' => $self->{account},
    695 'secure' => Digest::MD5::md5_hex( $date . "&" . $self->{key} ),
    696 };
    697 }
    698
    699 return $headers;
    700 }
    701
    702 sub getRequestValue {
    703 my ($self, $args, $key, $default) = @_;
    704 $args //= {};
    705
    706 my $out = {};
    707 foreach my $k ( keys %$args ) {
    708 if ( $k eq $key ) {
    709 return $self->getValue($args, $key, $default);
    710 } elsif ( $k =~ /^$key/ && index($k, '[') > 0 ) {
    711 my @levels;
    712 while ( $k =~ /\[(.*?)\]/g ) {
    713 my $name = $1;
    714 push @levels, { type => $name =~ /\D/ ? 'hash' : 'array', key => $name };
    715 }
    716 my $data = $out;
    717 for ( my $i = 0; $i < scalar @levels; $i++ ) {
    718 my $curr = $levels[$i];
    719 my $next = $i+1 < scalar @levels ? $levels[$i+1] : undef;
    720 if ( $next ) {
    721 if ( $curr->{type} eq 'hash' && !exists $data->{$curr->{key}} ) {
    722 $data->{$curr->{key}} = $next->{type} eq 'hash' ? {} : [];
    723 } elsif ( $curr->{type} eq 'array' && !defined $data->[$curr->{key}] ) {
    724 $data->[$curr->{key}] = $next->{type} eq 'hash' ? {} : [];
    725 }
    726 } else {
    727 $data->{$curr->{key}} = $args->{$k};
    728 last;
    729 }
    730 $data = $curr->{type} eq 'array' ? $data->[$curr->{key}] : $data->{$curr->{key}};
    731 }
    732 }
    733 }
    734
    735 return scalar %$out ? $out : $default;
    736 }
    737
    738 sub trim {
    739 my $val = shift;
    740
    741 for ( $val ) {
    742 s/^\s+//;
    743 s/\s+$//;
    744 }
    745
    746 return $val;
    747 }
    748
    749 1;

Небольшая справка по веткам

cnddist – контейнер, в котором хранятся все дистрибутивы всех библиотек и программных пакетов, которые использовались при построении различных версий Contenido. Если какой-то библиотеки в данном хранилище нет, инсталлятор сделает попытку "подтянуть" ее с веба (например, с CPAN). Если библиотека слишком старая, есть очень большая вероятность, что ее там уже нет. Поэтому мы храним весь хлам от всех сборок. Если какой-то дистрибутив вдруг отсутствует в cnddist - напишите нам, мы положим его туда.

koi8 – отмирающая ветка, чей код, выдача и все внутренние библиотеки заточены на кодировку KOI8-R. Вносятся только те дополнения, которые касаются внешнего вида и функционала админки, баги ядра, обязательные обновления портов и мелочи, которые легко скопипастить. В дальнейшем планируется полная остановка поддержки по данной ветке.

utf8 – актуальная ветка, заточенная под UTF-8.

Внутри каждой ветки: core – исходники ядра; install – скрипт установки инсталляции; plugins – плагины; samples – "готовые к употреблению" проекты, которые можно поставить, запустить и посмотреть, как они работают.