İçindekiler
- RAP Nedir, Neyi Değiştirdi?
- Bir RAP İş Nesnesinin Katmanları
- Managed, Unmanaged, Draft: Hangisi?
- Adım 1: Tablo ve Interface View
- Adım 2: Behavior Definition
- Adım 3: Behavior Implementation
- Adım 4: Projection, Servis ve Yetki
- EML: İş Nesnesini Koddan Kullanmak
- Mevcut BAPI'lere Dayanan Nesneler: Unmanaged Save
- RAP Nesnesini Test Etmek
- Sık Yapılan Hatalar
- Sonuç
Klasik ABAP'ta bir tabloya kayıt yazan uygulama genellikle şöyle büyür: bir Dynpro ya da SEGW servisi, içine gömülü doğrulamalar, elle yazılmış kilit yönetimi, COMMIT WORK'ün nerede çağrılacağına dair dağınık kararlar. Her yeni tüketici — bir Fiori uygulaması, bir arka plan işi, bir entegrasyon — aynı kuralları tekrar yazar ya da atlar.
RAP (ABAP RESTful Application Programming Model) bu dağınıklığı tek bir modelle değiştirir: veri CDS'te, davranış behavior definition'da, kurallar behavior implementation'da yaşar. Bütün tüketiciler aynı iş nesnesinden geçer. Servis tarafını daha önce OData V4 ve CDS yazısında anlatmıştım; bu yazı işlemsel davranışa, yani işin asıl zor kısmına odaklanıyor.
RAP Nedir, Neyi Değiştirdi?
RAP, S/4HANA'da OData servisleri ve işlemsel uygulamalar için SAP'nin önerdiği programlama modelidir. Daha önceki yaklaşımları — SEGW ile elle yazılan Gateway servislerini ve CDS + BOPF tabanlı programlama modelini — tek bir çatı altında birleştirir.
- Tek iş nesnesi, çok tüketici: Fiori uygulaması, Web API, arka plan işi ve başka bir ABAP programı aynı nesneyi aynı kurallarla kullanır.
- İşlem yönetimi framework'te: Kilit, ETag ile eşzamanlılık kontrolü, tampon ve kaydetme sırası RAP tarafından yürütülür. Siz
COMMIT WORKyazmazsınız. - Davranış bildirimsel: Hangi alanın salt okunur olduğu, hangi aksiyonun var olduğu ve hangi kuralın ne zaman çalışacağı kodda değil, behavior definition'da tanımlanır.
- ABAP Cloud'un işlemsel modeli: ABAP Cloud ve clean core ile geliştirirken işlemsel uygulama yazmanın yolu RAP'tir; Dynpro ve SEGW bu dil sürümünde yoktur.
Bir RAP İş Nesnesinin Katmanları
RAP'e ilk bakışta en çok kafa karıştıran şey, tek bir ekran için oluşturulan nesne sayısıdır. Her birinin net bir görevi var:
Veri modeli
Veritabanı tablosu veriyi tutar. Interface (R_) CDS view iş nesnesinin yeniden kullanılabilir veri modelidir; alan adları iş diline çevrilir, ilişkiler (composition, association) burada kurulur.
Davranış
Behavior definition (BDEF) davranışın sözleşmesidir: oluştur/değiştir/sil, aksiyonlar, doğrulamalar, kilit ve draft. Behavior pool adlı ABAP sınıfı bu sözleşmenin kodunu içerir.
Dışarıya açılım
Projection (C_) view ve projection BDEF, iş nesnesinin belirli bir kullanım için açılan dilimidir. Service definition hangi varlıkların açılacağını, service binding ise protokolü (OData V4 UI, OData V4 Web API…) belirler. Access control (DCL) okuma yetkisini yönetir.
Managed, Unmanaged, Draft: Hangisi?
Behavior definition'ın ilk satırı, nesnenin uygulama türünü belirler. Bu karar, kodun ne kadarını sizin yazacağınızı belirlediği için baştan doğru verilmelidir:
| Tür | Tamponu ve kaydetmeyi kim yönetir? | Ne zaman? |
|---|---|---|
managed | RAP; siz yalnızca iş kurallarını yazarsınız | Yeni geliştirme, kendi tablolarınız — varsayılan seçim |
managed + with additional save | RAP kaydeder; siz kaydetme anında ek iş yaparsınız | Değişiklik kaydı, olay yayınlama, ek tabloya yazma |
managed + with unmanaged save | RAP tamponu yönetir; kalıcılaştırmayı siz yaparsınız | Verinin mevcut bir API veya fonksiyon üzerinden yazılması gerektiğinde |
unmanaged | Tampon da kaydetme de sizde | Kendi tamponu ve işlem mantığı olan mevcut uygulamayı sarmalarken |
Draft ayrı bir eksendir ve her türle birleşebilir. Draft açık olduğunda kullanıcının kaydedilmemiş değişiklikleri ayrı bir draft tablosunda tutulur: tarayıcı kapansa bile kaybolmaz, doğrulamalar kullanıcı yazarken çalışabilir ve aynı kaydı başka biri düzenlemeye kalktığında "şu kullanıcı düzenliyor" kilidi devreye girer. Fiori Elements ile yazılan işlemsel uygulamalarda draft neredeyse her zaman doğru seçimdir.
Bu yazıdaki örnek: managed, draft destekli bir izin talebi. Çalışan izin talebi oluşturur, tarihler doğrulanır, yönetici onaylar ya da reddeder.
Adım 1: Tablo ve Interface View
Tablo, RAP'in yönetici alanlarını (oluşturan, son değiştiren, zaman damgaları) içerir. Bu alanlar ETag ve draft için gereklidir; RAP bunları kendisi doldurur:
@EndUserText.label : 'İzin talepleri'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zleave_req {
key client : abap.clnt not null;
key request_uuid : sysuuid_x16 not null;
employee_id : abap.char(8);
begin_date : abap.dats;
end_date : abap.dats;
status : abap.char(1);
local_created_by : abp_creation_user;
local_created_at : abp_creation_tstmpl;
local_last_changed_by : abp_locinst_lastchange_user;
local_last_changed_at : abp_locinst_lastchange_tstmpl;
last_changed_at : abp_lastchange_tstmpl;
}Interface view, iş nesnesinin kök varlığıdır. @Semantics annotation'ları RAP'e hangi alanı otomatik dolduracağını söyler:
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'İzin talebi'
define root view entity ZR_LeaveRequest
as select from zleave_req
{
key request_uuid as RequestUuid,
employee_id as EmployeeId,
begin_date as BeginDate,
end_date as EndDate,
status as Status,
@Semantics.user.createdBy: true
local_created_by as LocalCreatedBy,
@Semantics.systemDateTime.createdAt: true
local_created_at as LocalCreatedAt,
@Semantics.user.localInstanceLastChangedBy: true
local_last_changed_by as LocalLastChangedBy,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
local_last_changed_at as LocalLastChangedAt,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt
}Gerçek bir nesnede kalemler de olurdu: composition [0..*] of ZR_LeaveRequestDay as _Day gibi bir composition ile alt varlık tanımlanır ve kilit, draft ve yetki kök varlıktan devralınır. Bu yazıda yapıyı tek varlıkta tutuyorum; başlık–kalem yapısını RAP proje örneğinde bir masraf formu üzerinden kurdum.
Adım 2: Behavior Definition
Behavior definition, nesnenin davranış sözleşmesidir. Okuması kolaydır ve iş birimiyle konuşurken bile ekrana açılabilecek kadar açıktır:
managed implementation in class zbp_r_leaverequest unique;
strict ( 2 );
with draft;
define behavior for ZR_LeaveRequest alias LeaveRequest
persistent table zleave_req
draft table zleave_req_d
lock master
total etag LastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
{
create;
update;
delete;
field ( numbering : managed, readonly ) RequestUuid;
field ( readonly ) Status, LocalCreatedBy, LocalCreatedAt,
LocalLastChangedBy, LocalLastChangedAt, LastChangedAt;
field ( mandatory ) EmployeeId, BeginDate, EndDate;
determination setInitialStatus on modify { create; }
validation validateDates on save { create; field BeginDate, EndDate; }
action ( features : instance ) approve result [1] $self;
action ( features : instance ) reject result [1] $self;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare { validation validateDates; }
mapping for zleave_req
{
RequestUuid = request_uuid;
EmployeeId = employee_id;
BeginDate = begin_date;
EndDate = end_date;
Status = status;
LocalCreatedBy = local_created_by;
LocalCreatedAt = local_created_at;
LocalLastChangedBy = local_last_changed_by;
LocalLastChangedAt = local_last_changed_at;
LastChangedAt = last_changed_at;
}
}Satır satır önemli olanlar:
strict ( 2 ): Derleyicinin en sıkı kontrollerini açar; kullanımdan kalkmış söz dizimini ve tutarsız tanımları hata sayar. Yeni nesnelerde her zaman açık olmalı.draft table: Draft verisinin tutulacağı tablo. Elle yazmanıza gerek yok; ADT'de bu satırdaki uyarının hızlı düzeltmesi (quick fix) tabloyu oluşturur.lock master,etag: Kilit kök varlıkta alınır.etag masteriki kullanıcının aynı kaydı üst üste yazmasını engeller;total etagise draft ile aktif kayıt arasındaki tutarlılığı korur.numbering : managed: UUID anahtarı RAP üretir. Okunabilir bir talep numarası gerekiyorsa numara aralığından alınan ayrı bir alan olarak eklenir.- Determination ve validation:
setInitialStatuskayıt oluşturulduğunda çalışır;validateDateskaydetme anında ve tarih alanlarından biri değiştiyse çalışır.Prepareiçinde listelendiği için draft aşamasında da tetiklenir — kullanıcı hatayı "Kaydet"e basmadan görür. features : instance: Aksiyonun her kayıt için ayrı ayrı açılıp kapanabileceğini söyler. Hangi durumda açık olduğu kodda belirlenir.
Adım 3: Behavior Implementation
Behavior pool (zbp_r_leaverequest), ADT'nin BDEF'ten ürettiği iskeletle başlar. Kod, sınıfın local types bölümündeki bir handler sınıfında yazılır:
CLASS lhc_leaverequest DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
CONSTANTS:
BEGIN OF status,
new TYPE c LENGTH 1 VALUE 'N',
approved TYPE c LENGTH 1 VALUE 'A',
rejected TYPE c LENGTH 1 VALUE 'R',
END OF status.
METHODS get_instance_features FOR INSTANCE FEATURES
IMPORTING keys REQUEST requested_features FOR LeaveRequest RESULT result.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR LeaveRequest RESULT result.
METHODS set_initial_status FOR DETERMINE ON MODIFY
IMPORTING keys FOR LeaveRequest~setInitialStatus.
METHODS validate_dates FOR VALIDATE ON SAVE
IMPORTING keys FOR LeaveRequest~validateDates.
METHODS approve FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~approve RESULT result.
METHODS reject FOR MODIFY
IMPORTING keys FOR ACTION LeaveRequest~reject RESULT result.
ENDCLASS.Determination: başlangıç durumu
Determination, bir olaya tepki olarak alan değeri hesaplar. Burada yeni talebin durumu "Yeni" olarak atanır. IN LOCAL MODE, nesnenin kendi içinden yapılan erişimlerde yetki ve feature control kontrollerini atlar — salt okunur Status alanını ancak böyle değiştirebilirsiniz:
METHOD set_initial_status.
READ ENTITIES OF zr_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( Status ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DELETE requests WHERE Status IS NOT INITIAL.
CHECK requests IS NOT INITIAL.
MODIFY ENTITIES OF zr_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( Status )
WITH VALUE #( FOR request IN requests
( %tky = request-%tky
Status = status-new ) ).
ENDMETHOD.%tky (transactional key), anahtarın yanında kaydın draft mı aktif mi olduğunu da taşır. Draft destekli nesnelerde anahtarları %key ile değil %tky ile taşımak bu yüzden önemlidir.
Validation: tarih kontrolü
Validation hiçbir şeyi değiştirmez; yalnızca hatalı kayıtları failed tablosuna, mesajları reported tablosuna yazar. Mesaj alanlara bağlanırsa Fiori Elements ilgili alanı kırmızıyla işaretler:
METHOD validate_dates.
READ ENTITIES OF zr_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( BeginDate EndDate ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
LOOP AT requests INTO DATA(request).
" Önceki çalıştırmadan kalan mesajı temizle
APPEND VALUE #( %tky = request-%tky
%state_area = 'VALIDATE_DATES' ) TO reported-leaverequest.
IF request-EndDate < request-BeginDate.
APPEND VALUE #( %tky = request-%tky ) TO failed-leaverequest.
APPEND VALUE #( %tky = request-%tky
%state_area = 'VALIDATE_DATES'
%msg = new_message(
id = 'ZLEAVE'
number = '001'
severity = if_abap_behv_message=>severity-error )
%element-BeginDate = if_abap_behv=>mk-on
%element-EndDate = if_abap_behv=>mk-on ) TO reported-leaverequest.
ENDIF.
ENDLOOP.
ENDMETHOD.%state_area draft'ta önemlidir: aynı doğrulama tekrar çalıştığında önceki mesajın temizlenmesini sağlar. Bunu atlarsanız kullanıcı hatayı düzelttiği hâlde eski mesaj ekranda kalır.
Action ve feature control: onay
Aksiyon, standart oluştur/değiştir/sil dışındaki iş adımıdır. Onay aksiyonu durumu değiştirir ve güncel kaydı sonuç olarak döndürür; Fiori Elements ekranı bu sonuçla yeniler:
METHOD approve.
MODIFY ENTITIES OF zr_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
UPDATE FIELDS ( Status )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
Status = status-approved ) ).
READ ENTITIES OF zr_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(requests).
result = VALUE #( FOR request IN requests
( %tky = request-%tky
%param = request ) ).
ENDMETHOD.
" reject aynı kalıpla status-rejected yazar
METHOD get_instance_features.
READ ENTITIES OF zr_leaverequest IN LOCAL MODE
ENTITY LeaveRequest
FIELDS ( Status ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
result = VALUE #( FOR request IN requests
LET is_open = COND #( WHEN request-Status = status-new
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
IN ( %tky = request-%tky
%action-approve = is_open
%action-reject = is_open ) ).
ENDMETHOD.Feature control yalnızca düğmeyi gri yapmaz: devre dışı bir aksiyon başka bir yoldan — örneğin bir Web API'den ya da EML ile — çağrıldığında da RAP onu reddeder. Kural tek yerde ve her tüketici için geçerlidir.
get_instance_authorizations, değiştirme ve aksiyon yetkisini kontrol eder (örneğin yalnız yöneticinin onaylayabilmesi). Okuma yetkisi ise ayrı bir katmanda, aşağıdaki access control ile yönetilir. İkisi birbirinin yerine geçmez.Adım 4: Projection, Servis ve Yetki
Projection view, Fiori uygulaması için açılacak alanları seçer. provider contract transactional_query, bu projeksiyonun işlemsel bir servis için olduğunu söyler:
@AccessControl.authorizationCheck: #CHECK
@Metadata.allowExtensions: true
@EndUserText.label: 'İzin talebi - projeksiyon'
define root view entity ZC_LeaveRequest
provider contract transactional_query
as projection on ZR_LeaveRequest
{
key RequestUuid,
EmployeeId,
BeginDate,
EndDate,
Status,
LocalLastChangedAt,
LastChangedAt
}Projection BDEF, interface katmanındaki davranışın hangi kısmının bu servise açılacağını seçer. Burada yeni kural yazılmaz:
projection;
strict ( 2 );
use draft;
define behavior for ZC_LeaveRequest alias LeaveRequest
use etag
{
use create;
use update;
use delete;
use action approve;
use action reject;
use action Edit;
use action Activate;
use action Discard;
use action Resume;
use action Prepare;
}Service definition ve okuma yetkisi:
@EndUserText.label: 'İzin talebi servisi'
define service ZUI_LEAVEREQUEST_O4 {
expose ZC_LeaveRequest as LeaveRequest;
}@EndUserText.label: 'İzin talebi okuma yetkisi'
@MappingRole: true
define role ZR_LEAVEREQUEST {
grant select on ZR_LeaveRequest
where ( EmployeeId ) = aspect pfcg_auth( ZLEAVE, ZEMPLOYEE, ACTVT = '03' );
}Projeksiyon için ayrı bir rol yazıp koşulları kopyalamak yerine inheriting conditions from entity ZR_LeaveRequest ile devralmak, iki katmanın yetkisinin zamanla birbirinden kopmasını önler.
Son adım, service definition'dan bir OData V4 - UI service binding oluşturup yayımlamaktır. Binding'deki Preview ile, ayrı bir frontend projesi açmadan çalışan bir List Report / Object Page uygulaması görürsünüz. Ekranın annotation'larla nasıl şekillendirildiğini Fiori Elements mi, Freestyle mı? yazısında anlattım.
EML: İş Nesnesini Koddan Kullanmak
EML (Entity Manipulation Language), RAP nesnelerine ABAP içinden erişmenin dilidir. Bir arka plan işi, bir dosya yüklemesi veya başka bir iş nesnesi izin talebi oluşturacaksa tabloya doğrudan INSERT yazmaz; EML ile nesneyi kullanır — ve bütün doğrulamalar, determination'lar ve yetkiler devreye girer:
MODIFY ENTITIES OF zr_leaverequest
ENTITY LeaveRequest
CREATE FIELDS ( EmployeeId BeginDate EndDate )
WITH VALUE #( ( %cid = 'REQ1'
EmployeeId = '00001234'
BeginDate = '20261110'
EndDate = '20261114' ) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
IF failed IS INITIAL.
COMMIT ENTITIES
RESPONSE OF zr_leaverequest
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
ENDIF.İki ayrıntı sık atlanır. Birincisi: validateDates kaydetme anında çalıştığı için tarih hatası MODIFY'dan değil, COMMIT ENTITIES'in commit_failed sonucundan döner; ikisini de kontrol etmelisiniz. İkincisi: COMMIT ENTITIES yalnızca nesnenin dışında çağrılır. Behavior implementation içinde commit yazmak RAP işlem modelini bozar ve çalışma anında hata verir.
Mevcut BAPI'lere Dayanan Nesneler: Unmanaged Save
Her nesne sıfırdan kendi tablosuyla başlamaz. Verinin bir SAP standart nesnesine ait olduğu ve yalnızca belirli bir API üzerinden yazılabildiği durumlarda with unmanaged save iyi bir orta yoldur: RAP tamponu, kilidi ve draft'ı yönetmeye devam eder, kalıcılaştırmayı siz yaparsınız.
managed implementation in class zbp_r_deliverynote unique;
strict ( 2 );
with unmanaged save;CLASS lsc_deliverynote DEFINITION INHERITING FROM cl_abap_behavior_saver.
PROTECTED SECTION.
METHODS save_modified REDEFINITION.
ENDCLASS.
CLASS lsc_deliverynote IMPLEMENTATION.
METHOD save_modified.
LOOP AT update-deliverynote INTO DATA(note).
" Kalıcılaştırma burada: released bir API veya yerel sarmalayıcı
zcl_delivery_note_api=>update( note ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.save_modified içinde COMMIT WORK yazılmaz, mesaj kutusu açılmaz ve kendi içinde commit eden bir fonksiyon çağrılmaz. Commit'i RAP yapar. Kendi içinde commit eden eski bir BAPI'yi buraya koymak, işlemin yarısının yazılıp yarısının yazılmamasına yol açabilir. ABAP Cloud'da release edilmemiş bir BAPI'ye erişmeniz gerekiyorsa yol, ABAP Cloud yazısında anlattığım Tier 2 sarmalayıcıdır.RAP Nesnesini Test Etmek
RAP'in en az konuşulan faydası test edilebilirliktir. Determination, validation ve action'lar küçük, tek görevli metotlardır; EML ile çağrılabilir ve sonuçları failed / reported üzerinden doğrulanabilir. Bir test sınıfı şu iki soruyu cevaplamalıdır:
- Bitiş tarihi başlangıçtan önceyse kayıt reddediliyor ve mesaj doğru alana bağlanıyor mu?
- Onaylanmış bir talepte onay aksiyonu kapalı mı?
Veritabanına dokunmadan test etmek için CDS test double framework'ü ve RAP için sunulan EML test double'ları kullanılır. Bağımlılık kırma ve test double fikrinin temelini ABAP Unit yazısında anlattım; RAP'te değişen yalnızca araçlardır, yaklaşım aynıdır.
Sık Yapılan Hatalar
İş kuralını projeksiyona yazmak
Projection BDEF'e eklenen doğrulama yalnız o servise uygulanır; aynı nesneyi EML ile kullanan arka plan işi bu kuralı hiç görmez. Kurallar interface katmanına aittir.
Her doğrulamayı determination'a dönüştürmek
Determination veriyi değiştirir, validation yalnızca karar verir. Hatalı veriyi "sessizce düzelten" determination'lar kullanıcıyı şaşırtır ve hatayı gizler. Kullanıcının bilmesi gereken her şey validation ve mesajdır.
Tetikleyici alanları eksik bırakmak
on save { create; field BeginDate, EndDate; } gibi bir tanımda bir alanı unutursanız, o alan değiştiğinde doğrulama çalışmaz. Kural yeni bir alana dayandığında tetikleyici listesini de güncelleyin.
failed ve reported'ı yok saymak
EML çağrısı istisna fırlatmaz; hata, dönüş tablolarında gelir. Bunları kontrol etmeyen kod, başarısız bir işlemi başarılı sanır. Özellikle arka plan işlerinde bu, kimsenin fark etmediği kayıp kayıtlar demektir.
Tabloya doğrudan yazmak
"Hızlı olsun" diye RAP nesnesinin tablosuna doğrudan UPDATE yazmak; kilidi, ETag'i, doğrulamaları ve draft'ı atlar. RAP nesnesine sahip bir tabloya yazmanın tek doğru yolu EML'dir.
Sonuç
RAP'in ilk izlenimi "bir ekran için çok fazla nesne" olabilir. Ama bu nesnelerin her biri, klasik geliştirmede dağınık hâlde ve çoğu zaman tekrar tekrar yazılan bir sorumluluğu üstlenir: veri modeli, davranış sözleşmesi, iş kuralları, dışarıya açılım ve yetki. Karşılığında aynı kuralları her tüketiciye uygulayan, test edilebilir ve S/4HANA'nın yükseltme modeliyle uyumlu bir iş nesnesi elde edersiniz.
Başlangıç için önerim: kendi tablosu olan, küçük ama gerçek bir süreçle başlayın — bir talep, bir onay, bir bakım listesi. Managed ve draft ile kurun, kuralları validation ve action olarak yazın, en az iki testle kapatın. İkinci nesne, birincinin yarı süresinde çıkacaktır. Geliştirme tarafında sunduğum kapsamı SAP geliştirme hizmet sayfasında bulabilirsiniz.
RAP iş nesnesi tasarımı, mevcut geliştirmelerin RAP'e taşınması ve ekip içi rehberlik için destek alın.
Ön Görüşme Talep Et